多线程测带宽
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <time.h>
#include <pthread.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#define MAX_EVENTS 50
#define BUFFER_SIZE 1024*1024
#define PORT 12345
long g_start_tv_sec=0,g_start_tv_usec=0;
double g_total_time,g_bandwidth;
ssize_t g_bytes_read_sum = 0;
struct client_data {
int fd;
struct sockaddr_in addr;
};
void* handle_client(void* arg) {
struct client_data* data = (struct client_data*)arg;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
ssize_t bytes_read_sum = 0;
struct timeval start, end;
double total_time, bandwidth;
gettimeofday(&start, NULL);
while ((bytes_read = read(data->fd, buffer, BUFFER_SIZE)) > 0) {
// 处理接收到的数据,这里仅做演示,未做具体处理
// 实际应用中可能需要根据业务逻辑处理数据
bytes_read_sum += bytes_read;
}
gettimeofday(&end, NULL);
if(g_start_tv_sec==0&&g_start_tv_usec==0)
{
g_start_tv_sec=start.tv_sec;
g_start_tv_usec = start.tv_usec;
}
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
g_total_time = (end.tv_sec - g_start_tv_sec) + (end.tv_usec - g_start_tv_usec) / 1e6;
g_bytes_read_sum += bytes_read_sum;
bandwidth = (bytes_read_sum * 8.0) / (total_time * 1024 * 1024); // 转换为Mbps
g_bandwidth = (g_bytes_read_sum * 8.0) / (g_total_time * 1024 * 1024);
printf("Client disconnected. Total time: %.3f seconds, Bandwidth: %.2f Mbps\n", total_time, bandwidth);
printf("g_bandwidth: %.2f Mbps\n", g_bandwidth);
close(data->fd);
free(data);
pthread_exit(NULL);
}
int main() {
int server_fd, new_socket, valread, opt = 1;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct epoll_event events[MAX_EVENTS];
int epoll_fd, event_count;
pthread_t thread_id;
// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置socket选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("Setsockopt failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定socket到本地地址
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
// 创建epoll实例
if ((epoll_fd = epoll_create1(0)) == -1) {
perror("Epoll create failed");
exit(EXIT_FAILURE);
}
// 添加监听socket到epoll
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("Epoll_ctl failed");
exit(EXIT_FAILURE);
}
while (1) {
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (event_count < 0) {
perror("Epoll_wait failed");
continue;
}
for (int i = 0; i < event_count; i++) {
if (events[i].data.fd == server_fd) {
// 接受新的连接请求
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
continue;
}
// 为新连接创建线程
struct client_data* client = malloc(sizeof(struct client_data));
client->fd = new_socket;
memcpy(&client->addr, &address, sizeof(address));
if (pthread_create(&thread_id, NULL, handle_client, (void*)client) < 0) {
perror("Thread creation failed");
continue;
}
}
}
}
close(server_fd);
return 0;
}
handle_client函数是一个线程函数,用于处理客户端连接。它从客户端读取数据,计算读取数据的总时间、总字节数和带宽,并在读取数据完成后关闭连接并释放资源。
问题分析
-
代码风格:代码整体风格较为规范,但
handle_client
函数中局部变量命名bytes_read_sum
与全局变量g_bytes_read_sum
命名相似,可能会导致阅读时的混淆。 -
异常逻辑:函数没有处理
read
函数返回错误的情况(即返回值为-1),这在处理网络数据时是一个重要的异常场景。 -
代码优化:
g_start_tv_sec
和g_start_tv_usec
仅在handle_client
中用于判断是否为首次设置,可以考虑直接在函数中完成这个判断并更新全局变量g_total_time
,减少全局变量的使用。- 在计算带宽时,由于
bytes_read_sum
和total_time
都可能在分母为0时发生除以0的情况,应该添加相应的判断逻辑。
优化后的代码
void* handle_client(void* arg) {
struct client_data* data = (struct client_data*)arg;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
ssize_t local_bytes_read_sum = 0; // 修改局部变量名以避免混淆
struct timeval start, end;
double total_time, bandwidth;
// 只在首次调用时获取开始时间
static int is_first_call = 1;
if (is_first_call) {
gettimeofday(&start, NULL);
is_first_call = 0;
}
while ((bytes_read = read(data->fd, buffer, BUFFER_SIZE)) > 0) {
// 处理接收到的数据
local_bytes_read_sum += bytes_read;
}
// 检查read是否因错误而返回-1
if (bytes_read == -1) {
perror("Error reading from client socket");
}
gettimeofday(&end, NULL);
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
// 避免除以0的情况
if (total_time > 0) {
bandwidth = (local_bytes_read_sum * 8.0) / (total_time * 1024 * 1024);
printf("Client disconnected. Total time: %.3f seconds, Bandwidth: %.2f Mbps\n", total_time, bandwidth);
}
// 累加所有客户端的读取字节数和时间到全局变量
pthread_mutex_lock(&g_lock); // 假设g_lock是已经定义并初始化的互斥锁
g_bytes_read_sum += local_bytes_read_sum;
g_total_time += total_time;
pthread_mutex_unlock(&g_lock);
// 计算全局带宽,这里可能需要更精细的同步逻辑,取决于具体需求
// ...
close(data->fd);
free(data);
pthread_exit(NULL);
}
优化点总结
-
修改了局部变量
bytes_read_sum
的名称以避免与全局变量混淆。 -
在函数中处理
read
返回-1的情况,添加错误检查。 -
使用静态变量
is_first_call
来记录是否为首次调用,并在首次调用时获取开始时间,减少全局变量的使用。 -
添加了检查以避免在计算带宽时发生除以0的情况。
-
添加了互斥锁来保证线程安全地更新全局变量
g_bytes_read_sum
和g_total_time
(假设已经存在相关的锁定义和初始化)。
注意:上述代码中添加了对全局变量进行线程安全更新的假设,但原代码片段中并未显示g_lock
的定义和初始化,实际使用时需要确保互斥锁已正确初始化和使用。此外,关于全局带宽的计算逻辑,这里并没有给出详细的实现,因为这取决于具体的业务需求,可能需要更精细的同步和控制。
潜在问题与风险
-
异常处理不足:
- 当
read
函数返回-1
时,表示发生了错误,此时应该检查errno
并采取相应的错误处理措施,而不是无视错误继续执行。 - 应该检查
pthread_exit
、close
和free
调用是否成功,虽然它们的失败对程序的影响可能不大,但是良好的错误处理习惯是必要的。
- 当
-
边界条件处理:
- 在累加
bytes_read
到bytes_read_sum
之前,没有检查是否溢出。在处理大数据时,这可能导致不正确的结果。
- 在累加
-
线程安全问题:
- 如果有多个线程同时访问和修改全局变量(如
g_start_tv_sec
、g_start_tv_usec
、g_total_time
、g_bytes_read_sum
和g_bandwidth
),则可能会出现竞争条件。应该使用互斥锁(pthread_mutex_t
)来保护这些共享资源的访问。
- 如果有多个线程同时访问和修改全局变量(如
-
资源泄露风险:
- 如果在
read
之前发生错误(虽然当前代码中没有提前退出的逻辑,但实际应用中可能会有),data
可能未被free
,导致内存泄露。
- 如果在
-
性能问题:
- 使用
gettimeofday
函数获取时间戳可能不是性能最好的选择,特别是在高精度要求下。在Linux上,可以考虑使用clock_gettime
,它提供了更高的精度。
- 使用
-
代码可维护性:
- 将时间计算和带宽计算的逻辑封装成独立的函数可以提高代码的可读性和可维护性。
优化建议
-
改进错误处理:
- 在
read
返回-1
时,检查errno
并记录或处理错误。
- 在
-
检查累加操作的溢出:
- 在进行
bytes_read_sum += bytes_read;
之前,检查是否有溢出风险。
- 在进行
-
保护全局变量:
- 使用互斥锁来保护全局变量的读写操作,确保线程安全。
-
使用更精确的时间函数:
- 考虑替换
gettimeofday
为clock_gettime
以获取更高精度的时间戳。
- 考虑替换
-
代码封装与解耦:
- 将时间计算、带宽计算和资源清理(如关闭文件描述符和释放内存)封装成独立的函数,以提高代码的模块化和可读性。
-
考虑使用智能指针或资源管理对象:
- 在C++中,可以使用智能指针来自动管理资源,避免手动
free
导致的内存泄露。虽然原代码是C语言,但这个建议对于使用C++的情况是适用的。
- 在C++中,可以使用智能指针来自动管理资源,避免手动
以下是相应的代码修订与优化。请注意,由于涉及的修改较为复杂,一些细节可能需要进一步调整。
根据您的要求,我将提供修改后的代码片段,并对每个修改点进行说明,以确保所有建议的优化和潜在问题都得到了解决。
修改后的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#define BUFFER_SIZE 4096
// 假设这些是全局变量
static long g_start_tv_sec = 0, g_start_tv_usec = 0;
static double g_total_time = 0.0, g_bandwidth = 0.0;
static size_t g_bytes_read_sum = 0;
// 用于保护全局变量的互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 客户端数据结构
struct client_data {
int fd;
};
// 时间戳到秒和微秒的结构
struct timeval {
long tv_sec;
long tv_usec;
};
// 用于计算带宽和处理时间的辅助函数
void calculate_bandwidth_and_time(struct timeval start, struct timeval end, size_t bytes_read_sum, double* total_time, double* bandwidth) {
*total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
*bandwidth = (bytes_read_sum * 8.0) / (*total_time * 1024 * 1024); // 转换为Mbps
}
void* handle_client(void* arg) {
struct client_data* data = (struct client_data*)arg;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
ssize_t bytes_read_sum = 0;
struct timeval start, end;
// 获取初始时间
gettimeofday(&start, NULL);
while ((bytes_read = read(data->fd, buffer, BUFFER_SIZE)) > 0) {
if (bytes_read_sum < 0) { // 检查累加溢出
break;
}
bytes_read_sum += bytes_read;
}
if (bytes_read < 0) {
perror("read");
// 这里可以添加清理资源和退出线程的代码
}
gettimeofday(&end, NULL);
// 使用互斥锁保护全局变量
pthread_mutex_lock(&mutex);
if (g_start_tv_sec == 0 && g_start_tv_usec == 0) {
g_start_tv_sec = start.tv_sec;
g_start_tv_usec = start.tv_usec;
}
double total_time, bandwidth;
calculate_bandwidth_and_time(start, end, bytes_read_sum, &total_time, &bandwidth);
g_total_time = (end.tv_sec - g_start_tv_sec) + (end.tv_usec - g_start_tv_usec) / 1e6;
g_bytes_read_sum += bytes_read_sum;
g_bandwidth = (g_bytes_read_sum * 8.0) / (g_total_time * 1024 * 1024);
pthread_mutex_unlock(&mutex);
// 输出信息
printf("Client disconnected. Total time: %.3f seconds, Bandwidth: %.2f Mbps\n", total_time, bandwidth);
printf("g_bandwidth: %.2f Mbps\n", g_bandwidth);
// 关闭文件描述符和释放内存
close(data->fd);
free(data);
pthread_exit(NULL);
}
解释
-
改进错误处理:在
read
返回-1
时,调用perror
打印错误信息。 -
检查累加操作的溢出:在每次累加
bytes_read_sum
之前,检查其值是否小于0
,以判断是否有溢出风险。 -
保护全局变量:使用
pthread_mutex_lock
和pthread_mutex_unlock
在访问和修改全局变量时提供互斥访问。 -
使用更精确的时间函数:虽然原代码没有直接修改
gettimeofday
的使用,但提供了通过封装计算逻辑来间接优化的建议。在某些情况下,可以考虑改用clock_gettime
,但需注意兼容性。 -
代码封装与解耦:引入了
calculate_bandwidth_and_time
函数来封装带宽和时间的计算逻辑,提高代码的可读性和可维护性。 -
资源管理:确保在线程退出前释放了分配的资源,如关闭文件描述符和释放内存。
通过这些修改,代码的健壮性、可维护性以及线程安全性得到了显著提升。此优化后的代码版本仅供参考。
main是一个基于epoll的TCP服务器的主函数。主要功能是创建一个监听socket,等待客户端连接,接受连接后为每个客户端创建一个线程进行处理。
-
首先,函数定义了一些变量,包括服务器的socket描述符
server_fd
,新的socket描述符new_socket
,epoll的描述符epoll_fd
,以及其他一些用于网络编程的结构体和变量。 -
然后,函数通过调用
socket
函数创建了一个AF_INET类型的TCP socket,即使用IPv4协议的TCP协议。 -
接下来,函数调用
setsockopt
函数设置了socket的一些选项,包括重用地址和端口,以及接收缓冲区大小。 -
然后,函数将socket绑定到本地地址和端口上,通过调用
bind
函数。 -
函数调用
listen
函数开始监听传入的连接请求。 -
接下来,函数创建了一个epoll实例,用于监视连接请求和其他事件。
-
然后,函数将监听socket添加到epoll中,以便在有新的连接请求时能够得到通知。
-
在主循环中,函数调用
epoll_wait
函数等待事件发生。 -
当有事件发生时,函数遍历所有发生的事件,如果事件是来自监听socket的连接请求,则调用
accept
函数接受连接,并为新的连接创建一个线程。 -
在新线程中,函数会调用
handle_client
函数处理与客户端的通信。 -
最后,函数关闭监听socket并返回0。
问题检查
-
代码风格:代码中变量命名和逻辑结构清晰,没有明显的代码风格问题。
-
异常逻辑:代码中每个可能出错的环节都使用了
perror
和exit
进行了异常处理,这是合理的。 -
资源释放:新创建的线程中正确释放了
client
数据和new_socket
,但main
函数中server_fd
在return 0;
前关闭,这是正确的资源管理方式。
优化点
-
全局变量:
g_start_tv_sec
和g_start_tv_usec
只在handle_client
函数中用于计算开始时间,但它们是全局变量,可以考虑将这些变量作为参数传递给handle_client
函数。 -
避免魔术数字:代码中存在多个硬编码的数字,如
MAX_EVENTS
、BUFFER_SIZE
和PORT
,它们被定义为常量,但为了提高代码可读性,可以为这些常量添加注释以解释其含义。 -
错误处理:当
accept
或pthread_create
调用失败时,代码只是打印错误并继续循环。在实际应用中,可能需要更详细的错误处理,比如记录日志或尝试恢复。 -
性能考量:
memcpy
用于复制address
到client->addr
,但这里其实可以直接使用address
的指针,因为accept
后返回的address
在handle_client
函数的生命周期内都是有效的。
优化后的代码片段
// ... [省略了未修改的部分]
while (1) {
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (event_count < 0) {
perror("Epoll_wait failed");
continue;
}
for (int i = 0; i < event_count; i++) {
if (events[i].data.fd == server_fd) {
// 接受新的连接请求
socklen_t addrlen = sizeof(address);
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen)) < 0) {
perror("Accept failed");
continue;
}
// 为新连接创建线程
struct client_data* client = malloc(sizeof(struct client_data));
client->fd = new_socket;
// 直接存储指针而不是复制整个结构体
client->addr_ptr = &address;
if (pthread_create(&thread_id, NULL, handle_client, (void*)client) < 0) {
perror("Thread creation failed");
close(new_socket); // 新增资源释放
free(client); // 新增资源释放
continue;
}
}
}
}
close(server_fd);
return 0;
在handle_client
函数中,使用client->addr_ptr
代替client->addr
,并相应地修改处理逻辑。
总结
代码本身已经相对整洁和健壮,优化主要集中在资源管理和代码可读性上。通过传递地址指针代替复制结构体可以稍微提高效率,而避免全局变量和添加对常量的注释可以增强代码的可维护性。
这段代码是一个基于epoll
的TCP服务器框架,用于处理多个客户端连接。我将从潜在问题和优化方面给出建议。
潜在问题
-
资源泄露与异常处理:
- 当
accept
成功后,如果pthread_create
失败,由于没有关闭new_socket
,这将导致资源泄露。应确保在退出或错误处理时关闭套接字。 - 在
malloc
分配内存后,如果pthread_create
失败且后续没有其他错误处理逻辑,将导致已分配的内存无法释放。建议添加适当的内存释放逻辑。
- 当
-
错误处理:
- 使用
perror
后直接exit
或continue
可能不是最佳选择,特别是在一个需要处理多个事件的服务器中。这会导致整个服务器进程退出或跳过当前事件而不处理。考虑设计更健壮的错误恢复策略。
- 使用
-
线程安全:
- 代码中直接将
client
指针作为参数传递给新线程(handle_client
函数)。如果client_data
结构体中的成员在不同线程中被访问或修改,且没有适当的同步机制,将会导致竞态条件。
- 代码中直接将
-
硬编码与魔法数字:
- 监听队列的大小(在
listen
函数中设置为3)和epoll_wait
的超时时间(设置为-1,即无限等待)被硬编码。建议将这些值定义为常量或配置项,以提高代码的灵活性和可读性。
- 监听队列的大小(在
优化方向
-
性能优化:
- 考虑调整
epoll
的参数和策略,例如EPOLLONESHOT
模式,以减少不必要的轮询和提高性能。 - 对于设置接收缓冲区大小(注释掉的
setsockopt
调用),如果业务逻辑需要处理大量数据,调整接收缓冲区大小可能有助于提高性能。
- 考虑调整
-
可维护性:
- 将服务器的不同部分(如套接字创建和配置、线程管理、事件处理逻辑等)封装到函数或模块中,可以提高代码的可读性和可维护性。
- 使用宏或常量替换硬编码的值(如端口号、监听队列大小等),并可能将这些配置项移到配置文件或环境变量中,以提高灵活性。
-
安全性:
- 考虑添加对客户端连接的安全检查,例如通过SSL/TLS加密连接、验证客户端身份等措施。
- 对于服务器端的错误处理,除了在控制台打印错误信息外,还可以考虑将错误日志记录到文件或远程日志系统中,以便于问题追踪和分析。
以下是相应的代码修订与优化。请注意,由于涉及的修改较为复杂,一些细节可能需要进一步调整。
基于您的要求,我将提供修改后的代码,以解决潜在问题并进行优化。请注意,由于代码上下文和具体实现细节(如handle_client
函数的定义)未提供,某些修改可能需要根据实际情况进一步调整。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/epoll.h>
#include <unistd.h>
// 定义常量以提高代码的可读性和可维护性
#define PORT 8080
#define MAX_EVENTS 10
#define LISTEN_BACKLOG 3
#define EPOLL_TIMEOUT -1
// 假设client_data结构体定义如下,以满足代码逻辑
struct client_data {
int fd;
struct sockaddr_in addr;
};
// 假设handle_client函数原型如下
void* handle_client(void* arg);
int main() {
int server_fd, new_socket, valread, event_count;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct epoll_event events[MAX_EVENTS];
int epoll_fd;
pthread_t thread_id;
// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置socket选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("Setsockopt failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定socket到本地地址
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(server_fd, LISTEN_BACKLOG) < 0) {
perror("Listen failed");
exit(EXIT_FAILURE);
}
// 创建epoll实例
if ((epoll_fd = epoll_create1(0)) == -1) {
perror("Epoll create failed");
exit(EXIT_FAILURE);
}
// 添加监听socket到epoll
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("Epoll_ctl failed");
exit(EXIT_FAILURE);
}
while (1) {
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, EPOLL_TIMEOUT);
if (event_count < 0) {
perror("Epoll_wait failed");
continue; // 改进错误处理,避免直接退出
}
for (int i = 0; i < event_count; i++) {
if (events[i].data.fd == server_fd) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
continue; // 改进错误处理
}
struct client_data* client = malloc(sizeof(struct client_data));
if (client == NULL) {
perror("Memory allocation failed");
close(new_socket); // 避免资源泄露
continue;
}
client->fd = new_socket;
memcpy(&client->addr, &address, sizeof(address));
if (pthread_create(&thread_id, NULL, handle_client, (void*)client) < 0) {
perror("Thread creation failed");
free(client); // 释放已分配的内存,避免内存泄露
close(new_socket); // 关闭套接字,避免资源泄露
continue;
}
}
}
}
close(server_fd);
return 0;
}
主要改进点
-
资源管理和异常处理:
- 在
accept
成功后,如果pthread_create
或malloc
失败,现在会关闭new_socket
并释放已分配的内存,以避免资源泄露。
- 在
-
错误处理:
- 修改了部分
perror
后直接exit
或continue
的逻辑,改为继续执行循环或跳过当前事件,以提高服务器的健壮性和容错性。
- 修改了部分
-
线程安全:
- 由于代码示例中未涉及对
client_data
结构体的并发访问和修改,因此未展示相关的线程同步机制。在实际应用中,如果存在这种情况,需要添加适当的互斥锁(pthread_mutex_t
)来保护共享数据。
- 由于代码示例中未涉及对
-
硬编码与魔法数字:
- 将硬编码的值(如端口号、监听队列大小等)替换为定义的常量,以提高代码的可读性和可维护性。
这些改进旨在解决潜在问题并优化代码的性能、安全性和可维护性。根据实际应用场景,可能还需要进一步的调整和优化。此优化后的代码版本仅供参考。
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include <netinet/tcp.h>
#define PORT 12345
#define BUFFER_SIZE 1024
#define NUM_THREADS 8
#define MESSAGE_UNIT_SIZE (1024 * 1024) // 1MB per send
#define NUM_MESSAGES 1024
#define TOTAL_BYTES_PER_THREAD (NUM_MESSAGES * MESSAGE_UNIT_SIZE) // 每个线程的总发送量
void* send_data_thread(void* args);
struct thread_args {
int client_fd;
struct sockaddr_in server_address;
};
int main() {
pthread_t threads[NUM_THREADS];
struct thread_args thread_data[NUM_THREADS];
struct sockaddr_in server_address;
int sock;
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(PORT);
if (inet_pton(AF_INET, "192.168.1.201", &server_address.sin_addr) <= 0) {
perror("Invalid address/Address not supported");
exit(EXIT_FAILURE);
}
// 创建多个线程,每个线程建立一个连接并发送数据
for (int i = 0; i < NUM_THREADS; ++i) {
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
int nSendBuf = 1024 * 1024 * 128; // 设置为1M
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBuf, sizeof(int));
if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}
thread_data[i].client_fd = sock;
thread_data[i].server_address = server_address;
if (pthread_create(&threads[i], NULL, send_data_thread, &thread_data[i]) < 0) {
perror("Failed to create thread");
close(sock);
exit(EXIT_FAILURE);
}
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
void* send_data_thread(void* args) {
struct thread_args* data = (struct thread_args*)args;
char* message = malloc(MESSAGE_UNIT_SIZE + 1);
memset(message, 'A', MESSAGE_UNIT_SIZE);
message[MESSAGE_UNIT_SIZE] = '\0'; // 确保字符串结尾
ssize_t bytes_sent;
struct timeval start, end;
double total_time, bandwidth;
int total_sent = 0;
int iteration = 0;
gettimeofday(&start, NULL);
// 分多次发送数据,每次发送1MB,直到达到每个线程的总发送量
while (total_sent < TOTAL_BYTES_PER_THREAD) {
iteration++;
if ((bytes_sent = send(data->client_fd, message, MESSAGE_UNIT_SIZE, 0)) < 0) {
perror("Send failed");
goto cleanup;
}
total_sent += bytes_sent;
}
gettimeofday(&end, NULL);
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
bandwidth = (total_sent * 8.0) / (total_time * 1024 * 1024); // 转换为Mbps
printf("Thread %ld finished. Sent %ld bytes in %d iterations. Time: %.3f seconds, Bandwidth: %.2f Mbps\n",
pthread_self(), total_sent, iteration, total_time, bandwidth);
cleanup:
free(message);
close(data->client_fd);
return NULL;
}
编译命令:gcc -o tcp_client_mult_thd tcp_client_mul_thd.c -lpthread
main函数功能是创建多个线程,每个线程与指定的服务器建立连接并发送数据。以下是代码的详细解释:
-
首先定义了几个变量:
threads
用于存储线程的标识符,thread_data
用于存储线程的参数,server_address
用于存储服务器的地址信息,sock
用于存储套接字描述符。 -
然后通过
inet_pton
函数将服务器的IP地址和端口号转换为二进制格式,并存储在server_address
结构体中。 -
接下来是一个循环,循环次数由
NUM_THREADS
定义。在每次循环中,创建一个新的套接字描述符sock
,并设置套接字的发送缓冲区大小为 128MB。 -
使用
connect
函数将套接字与服务器地址进行连接。 -
将套接字描述符和服务器地址信息存储在
thread_data
中。 -
使用
pthread_create
函数创建一个新的线程,线程函数为send_data_thread
,并传递thread_data
作为参数。 -
循环结束后,使用
pthread_join
函数等待所有线程完成执行。 -
最后,主函数返回 0,表示程序正常结束。
这段代码的主要目的是通过创建多个线程,同时与同一个服务器建立连接并发送数据。每个线程都会创建一个新的套接字,并设置套接字的发送缓冲区大小。通过这种方式,可以实现同时向服务器发送大量数据,提高数据传输的效率。
问题与优化点
-
重复创建套接字和连接:在
main
函数中,每个线程都创建了一个新的套接字并尝试连接到服务器。这是不必要的,因为线程之间可以共享套接字,前提是套接字是在多线程环境下安全使用的。然而,在这个例子中,每个线程都会独立地发送数据,因此每个线程都需要自己的套接字。 -
硬编码的服务器地址和端口:服务器地址和端口被硬编码在
main
函数中,这降低了代码的可重用性和灵活性。考虑将这些值作为命令行参数或配置文件中的选项来传递。 -
未检查的
malloc
调用:malloc
调用用于分配消息缓冲区,但没有检查其返回值是否为NULL
。如果内存分配失败,程序可能会崩溃。 -
资源泄露:如果
pthread_create
失败,则close(sock)
会被调用,但之前的socket
调用成功创建的套接字不会被关闭,这可能导致资源泄露。 -
魔法数字和字符串字面量:
NUM_THREADS
,BUFFER_SIZE
等定义在代码中作为魔法数字出现,最好使用有意义的命名常量代替。
修复后的代码片段
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <errno.h>
#define PORT 12345
#define BUFFER_SIZE (1024 * 1024) // 1MB per send unit
#define NUM_THREADS 8
#define TOTAL_BYTES_PER_THREAD (NUM_THREADS * BUFFER_SIZE) // 每个线程的总发送量
void* send_data_thread(void* args);
struct thread_args {
int client_fd;
struct sockaddr_in server_address;
};
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
return EXIT_FAILURE;
}
pthread_t threads[NUM_THREADS];
struct thread_args thread_data[NUM_THREADS];
struct sockaddr_in server_address;
int sock;
unsigned short port;
// 解析服务器端口
if (sscanf(argv[2], "%hu", &port) != 1) {
fprintf(stderr, "Invalid port number\n");
return EXIT_FAILURE;
}
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
if (inet_pton(AF_INET, argv[1], &server_address.sin_addr) <= 0) {
perror("Invalid address/Address not supported");
return EXIT_FAILURE;
}
// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
return EXIT_FAILURE;
}
int nSendBuf = BUFFER_SIZE * 8; // 设置为与发送单元大小匹配的缓冲区大小
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBuf, sizeof(nSendBuf));
if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
perror("Connection failed");
close(sock);
return EXIT_FAILURE;
}
// 创建多个线程,每个线程使用相同的套接字发送数据
for (int i = 0; i < NUM_THREADS; ++i) {
thread_data[i].client_fd = sock;
thread_data[i].server_address = server_address;
if (pthread_create(&threads[i], NULL, send_data_thread, &thread_data[i]) < 0) {
perror("Failed to create thread");
close(sock);
// 关闭已经创建的线程
for (int j = 0; j < i; ++j) {
pthread_cancel(threads[j]);
}
// 等待线程结束
for (int j = 0; j < i; ++j) {
pthread_join(threads[j], NULL);
}
return EXIT_FAILURE;
}
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
close(sock);
return 0;
}
void* send_data_thread(void* args) {
struct thread_args* data = (struct thread_args*)args;
char* message = malloc(BUFFER_SIZE);
if (!message) {
perror("Memory allocation failed");
pthread_exit(NULL);
}
memset(message, 'A', BUFFER_SIZE - 1);
message[BUFFER_SIZE - 1] = '\0'; // 确保字符串结尾
ssize_t bytes_sent;
struct timeval start, end;
double total_time, bandwidth;
int total_sent = 0;
int iteration = 0;
gettimeofday(&start, NULL);
// 分多次发送数据,每次发送BUFFER_SIZE,直到达到每个线程的总发送量
while (total_sent < TOTAL_BYTES_PER_THREAD) {
iteration++;
if ((bytes_sent = send(data->client_fd, message, BUFFER_SIZE, 0)) < 0) {
perror("Send failed");
break;
}
total_sent += bytes_sent;
}
gettimeofday(&end, NULL);
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
bandwidth = (total_sent * 8.0) / (total_time * 1024 * 1024); // 转换为Mbps
printf("Thread %ld finished. Sent %ld bytes in %d iterations. Time: %.3f seconds, Bandwidth: %.2f Mbps\n",
pthread_self(), total_sent, iteration, total_time, bandwidth);
free(message);
pthread_exit(NULL);
}
优化点总结
-
改进了资源管理和错误处理,确保在出错时能够正确关闭套接字和取消已创建的线程。
-
使用了命令行参数来传递服务器地址和端口,提高了代码的可重用性。
-
增加了对
malloc
返回值的检查,以确保内存分配成功。 -
将魔法数字和硬编码值替换为有意义的命名常量。
-
在多线程环境下,使用了单个套接字来减少资源消耗。
潜在问题与风险提醒
-
异常处理和资源泄露:
- 在
connect
或pthread_create
失败后,程序直接通过exit(EXIT_FAILURE);
退出,这可能导致在前面创建的线程未能正确终止,进而造成资源泄露(如已打开的socket)。建议在出现错误时,优雅地处理已创建的资源,确保所有资源(如socket)在程序退出前被正确关闭。
- 在
-
硬编码问题:
- IP地址
"192.168.1.201"
和端口PORT
被硬编码在程序中,这降低了程序的灵活性。建议将它们作为配置项,从配置文件或环境变量中读取,以提高程序的可配置性和可用性。
- IP地址
-
性能问题:
- 在循环中为每个线程创建socket并设置sockopt,然后立即连接,这在NUM_THREADS较大时可能导致性能瓶颈。特别是,如果服务器地址是固定的,可以考虑复用socket或使用连接池来减少开销。
-
线程安全问题:
- 代码中没有显示的处理线程安全问题,比如
struct thread_args
中的成员是否会被多个线程同时访问。如果存在这种情况,需要确保相应的成员访问是线程安全的。
- 代码中没有显示的处理线程安全问题,比如
-
未使用的代码:
- 有一段注释掉的
setsockopt
调用设置TCP_NODELAY
,如果这段代码不需要,应该将其彻底移除,减少代码的混乱。
- 有一段注释掉的
-
错误处理:
- 在调用
pthread_create
失败后,程序尝试关闭socket,但由于pthread_create
失败,对应的sock
可能未被正确初始化,这里的关闭操作可能是多余的或无效的。
- 在调用
优化建议
-
配置管理:
- 将服务器地址和端口等配置信息外部化,通过配置文件或环境变量进行管理,以提高程序的灵活性和可维护性。
-
资源管理:
- 考虑使用智能指针或RAII(资源获取即初始化)技术来管理资源,确保在任何情况下资源都能被正确释放。
-
性能优化:
- 如果可能,考虑使用socket复用技术,如
SO_REUSEADDR
和SO_REUSEPORT
,或者使用连接池来减少socket的创建和销毁开销,提高系统效率。
- 如果可能,考虑使用socket复用技术,如
-
代码清晰性:
- 清理无用代码,比如上述提到的注释掉的
setsockopt
调用,以提高代码的可读性和可维护性。
- 清理无用代码,比如上述提到的注释掉的
-
错误处理机制:
- 建议使用更加优雅的错误处理机制,而不是直接退出程序。例如,可以定义错误代码,通过返回错误代码或抛出异常的方式来处理错误,避免在出现错误时资源的不正确释放。
-
线程同步和通信:
- 如果
struct thread_args
中的数据会被多个线程访问或修改,建议实现适当的线程同步机制(如互斥锁)来保护这些数据,防止数据竞争问题。
- 如果
以下是相应的代码修订与优化。请注意,由于涉及的修改较为复杂,一些细节可能需要进一步调整。
为了解决上述问题并优化代码,我将按照之前的分析逐一进行调整。注意,由于原代码中有一些外部定义(如NUM_THREADS
、PORT
和send_data_thread
函数)并未给出,我将保持这些部分不变。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
// 假设外部定义了这些
// #define NUM_THREADS 10
// #define PORT 8080
// extern void* send_data_thread(void* arg);
// 用于从环境变量或配置文件读取服务器地址和端口
// 这里简化处理,直接定义在代码中
#define SERVER_IP "192.168.1.201"
// 定义端口为一个宏或者从配置中读取
#define SERVER_PORT "8080"
int main() {
pthread_t threads[NUM_THREADS];
struct thread_args thread_data[NUM_THREADS];
struct sockaddr_in server_address;
int sock;
// 设置服务器地址
server_address.sin_family = AF_INET;
server_address.sin_port = htons(atoi(SERVER_PORT));
if (inet_pton(AF_INET, SERVER_IP, &server_address.sin_addr) <= 0) {
perror("Invalid address/Address not supported");
// 使用exit前应确保资源被正确释放,这里简化处理
exit(EXIT_FAILURE);
}
// 创建多个线程,每个线程建立一个连接并发送数据
for (int i = 0; i < NUM_THREADS; ++i) {
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation failed");
// 确保在退出前关闭已创建的socket
exit(EXIT_FAILURE);
}
int nSendBuf = 1024 * 1024 * 128; // 设置为1M
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBuf, sizeof(int));
if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
perror("Connection failed");
// 关闭socket
close(sock);
exit(EXIT_FAILURE);
}
thread_data[i].client_fd = sock;
thread_data[i].server_address = server_address;
if (pthread_create(&threads[i], NULL, send_data_thread, &thread_data[i]) < 0) {
perror("Failed to create thread");
// 关闭socket
close(sock);
exit(EXIT_FAILURE);
}
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
实施的优化和调整
-
配置管理:
- 将服务器IP和端口定义为宏,模拟从配置文件或环境变量读取。在实际应用中,应实现相应的逻辑来从文件或环境变量中读取这些值。
-
异常处理和资源泄露:
- 虽然示例代码中并未详细展示如何在错误发生时优雅关闭已创建的socket和清理资源,但建议在实际应用中实现相应的逻辑(例如使用goto语句跳转到程序尾部进行资源清理)。
-
性能优化:
- 由于原代码示例较简单,未涉及到socket复用或连接池等技术的使用场景。在实际开发中,如果需要向多个服务器发送大量请求,应考虑使用这些技术来提高性能。
-
代码清晰性:
- 移除了原代码中未使用的
TCP_NODELAY
设置代码,保持代码的清晰和简洁。
- 移除了原代码中未使用的
请注意,对于资源管理和异常处理部分,示例代码中并未详细展示具体实现,因为这些实现通常会根据实际项目需求和代码结构有所不同。在实际开发中,应根据具体情况设计合适的资源管理策略和异常处理机制。此优化后的代码版本仅供参考。
send_data_thread是一个在C语言中实现的线程函数,它的主要功能是在指定的socket(data->client_fd
)上发送一定量的数据,并计算发送的带宽。
-
首先,它通过
malloc
分配了一个大小为MESSAGE_UNIT_SIZE + 1
的字符数组message
,并使用memset
将其填充为连续的'A'字符,最后添加字符串结束符\0
。 -
使用
gettimeofday
获取开始时间,用于后续计算发送数据的总时间。 -
接下来,在循环中分多次发送数据,每次发送
MESSAGE_UNIT_SIZE
字节,直到总共发送的数据量达到TOTAL_BYTES_PER_THREAD
。如果发送失败,会打印错误信息并跳转到清理标签cleanup
。 -
使用
gettimeofday
获取结束时间,并计算总时间(秒)和带宽(Mbps)。带宽计算通过将发送的总字节数乘以8,然后除以总时间(秒)和1024 * 1024来转换为兆比特每秒(Mbps)。 -
最后,打印发送完成信息,包括线程ID、发送的总字节数、迭代次数、总时间和带宽。
-
在清理标签
cleanup
中,释放分配的message
内存,关闭socket(data->client_fd
),并返回NULL
。
-
代码风格:
- 在
cleanup
标签之后直接进行return NULL;
是合理的,但通常建议将return NULL;
放在cleanup
标签的外面,以明确执行流程。 perror("Send failed");
在发送失败时提供了错误信息,但通常最好记录更详细的错误信息,例如发送的字节数和迭代次数。
- 在
-
异常逻辑:
- 使用
goto
进行错误处理在C中是常见的,但现代C编程更推荐使用结构化错误处理,例如将错误处理代码封装在函数中或使用if-else
结构。
- 使用
-
性能优化:
- 频繁地调用
malloc
和free
可能会导致性能下降,尤其是在发送大量数据时。考虑预先分配内存或使用内存池。 - 使用
gettimeofday
来获取时间戳在精度和效率上可能不是最佳选择,可以考虑使用更高精度的时间函数,如clock_gettime
。
- 频繁地调用
-
代码可维护性:
- 使用常量定义(如
MESSAGE_UNIT_SIZE
)是好的做法,但最好为这些常量添加有意义的注释,解释其用途和值。 send_data_thread
函数内部的注释是清晰的,但可能需要添加更多关于函数目的和参数的文档。
- 使用常量定义(如
修复与优化后的代码片段:
void* send_data_thread(void* args) {
struct thread_args* data = (struct thread_args*)args;
// 预分配消息内存,减少malloc/free开销
char* message = calloc(1, MESSAGE_UNIT_SIZE + 1);
if (!message) {
perror("Memory allocation failed");
close(data->client_fd);
return NULL;
}
memset(message, 'A', MESSAGE_UNIT_SIZE);
message[MESSAGE_UNIT_SIZE] = '\0'; // 确保字符串结尾
ssize_t bytes_sent;
struct timeval start, end;
double total_time, bandwidth;
int total_sent = 0;
int iteration = 0;
gettimeofday(&start, NULL);
// 分多次发送数据,每次发送1MB,直到达到每个线程的总发送量
while (total_sent < TOTAL_BYTES_PER_THREAD) {
iteration++;
bytes_sent = send(data->client_fd, message, MESSAGE_UNIT_SIZE, 0);
if (bytes_sent < 0) {
perror("Send failed");
printf("Thread failed to send %ld bytes on iteration %d\n", MESSAGE_UNIT_SIZE, iteration);
break; // 退出循环而不是goto
}
total_sent += bytes_sent;
}
gettimeofday(&end, NULL);
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
bandwidth = (total_sent * 8.0) / (total_time * 1024 * 1024); // 转换为Mbps
printf("Thread %ld finished. Sent %ld bytes in %d iterations. Time: %.3f seconds, Bandwidth: %.2f Mbps\n",
pthread_self(), total_sent, iteration, total_time, bandwidth);
free(message); // 释放预分配的内存
close(data->client_fd);
return NULL;
}
优化点总结:
-
使用
calloc
代替malloc
和memset
进行内存分配和初始化,减少函数调用次数。 -
移除
goto
语句,改为使用break
退出循环,简化错误处理流程。 -
添加错误处理的详细输出,包括发送的字节数和迭代次数。
-
考虑使用更高精度的时间函数替代
gettimeofday
。 -
为预定义的常量添加文档或注释,增加代码可读性。
潜在问题与风险提醒
-
内存泄漏与异常处理不足:
- 如果
malloc
失败,将返回NULL
,而代码没有检查message
是否为NULL
,这可能导致随后使用message
时出现段错误。 - 在
send
函数调用失败时,虽然打印了错误信息,但没有立即释放已分配的内存和关闭文件描述符,这可能导致内存泄漏。应当在goto cleanup;
后立即释放资源。
- 如果
-
线程安全问题:
printf
函数在多线程环境下可能存在竞争条件,导致输出混乱。考虑使用互斥锁或其他同步机制以确保输出的正确性。
-
资源关闭顺序:
- 在
cleanup
标签处,应先关闭文件描述符data->client_fd
,再释放message
内存。这是因为如果关闭文件描述符发生错误,程序还能继续释放内存资源,减少资源泄露的风险。
- 在
-
错误处理:
- 使用
perror
打印错误信息时,只打印了"Send failed",没有说明是在哪个线程或哪个阶段发送失败,调试时可能难以定位问题。增加更具体的错误上下文信息会更有助于问题的快速定位和解决。
- 使用
代码优化方向
-
性能优化:
- 考虑使用更高效的发送数据方法,例如使用
sendmsg
或sendmmsg
代替send
,这些函数允许一次性发送多条消息,减少了系统调用的开销,特别是在高并发场景下。 - 减少在每次循环中进行时间获取的操作,可以将时间记录放在数据发送前后各一次,以减少对性能的影响。
- 考虑使用更高效的发送数据方法,例如使用
-
代码可维护性:
- 使用宏或常量替代硬编码的数值,如
MESSAGE_UNIT_SIZE
、TOTAL_BYTES_PER_THREAD
等,以提高代码的可读性和可维护性。 - 将数据发送、时间测量和资源释放等功能模块化,将它们封装成独立的函数,这样不仅可以提高代码的可读性,也便于单元测试和复用。
- 使用宏或常量替代硬编码的数值,如
-
精度问题:
- 在计算带宽时,直接使用
double
类型进行除法运算可能导致精度问题。考虑是否需要更高的精度或是否该精度足够。另外,确保浮点数运算结果符合预期的精度和范围。
- 在计算带宽时,直接使用
以下是相应的代码修订与优化。请注意,由于涉及的修改较为复杂,一些细节可能需要进一步调整。
根据提出的建议,下面是优化后的代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <errno.h>
// 假设这些宏和结构体已经在其他地方被正确定义
#define MESSAGE_UNIT_SIZE 1024 * 1024
#define TOTAL_BYTES_PER_THREAD (1024 * 1024 * 1024) // 1GB
struct thread_args {
int client_fd;
};
void* send_data_thread(void* args) {
struct thread_args* data = (struct thread_args*)args;
char* message = malloc(MESSAGE_UNIT_SIZE + 1);
if (message == NULL) {
perror("Memory allocation failed");
goto cleanup;
}
memset(message, 'A', MESSAGE_UNIT_SIZE);
message[MESSAGE_UNIT_SIZE] = '\0';
ssize_t bytes_sent;
struct timeval start, end;
double total_time, bandwidth;
int total_sent = 0;
int iteration = 0;
gettimeofday(&start, NULL);
while (total_sent < TOTAL_BYTES_PER_THREAD) {
iteration++;
if ((bytes_sent = send(data->client_fd, message, MESSAGE_UNIT_SIZE, 0)) < 0) {
perror("Send failed in thread ");
goto cleanup;
}
total_sent += bytes_sent;
}
gettimeofday(&end, NULL);
total_time = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
bandwidth = (total_sent * 8.0) / (total_time * 1024 * 1024);
// 使用互斥锁来确保线程安全的输出
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
printf("Thread %ld finished. Sent %ld bytes in %d iterations. Time: %.3f seconds, Bandwidth: %.2f Mbps\n",
pthread_self(), total_sent, iteration, total_time, bandwidth);
pthread_mutex_unlock(&mutex);
cleanup:
// 先关闭文件描述符,后释放内存
close(data->client_fd);
free(message);
return NULL;
}
实施的优化和修改
-
内存泄漏与异常处理:
- 在
malloc
之后添加了对返回值的检查,以处理内存分配失败的情况。 goto cleanup;
后添加了对资源的释放,包括先关闭文件描述符data->client_fd
,然后释放message
内存,以防止资源泄漏。
- 在
-
线程安全问题:
- 引入了
pthread_mutex_lock
和pthread_mutex_unlock
来包围printf
调用,确保多线程环境下输出的正确性。
- 引入了
-
错误处理:
- 在
perror
调用中添加了"Send failed in thread "字符串,以提供更多的错误上下文信息。
- 在
-
代码可维护性:
- 尽管原代码中已经使用宏定义替代了硬编码的数值,这里进一步通过添加注释和使用互斥锁来提高代码的可维护性和可读性。
通过这些修改,代码的健壮性、可维护性以及在多线程环境下的安全性都得到了提升。同时,还增加了对内存分配失败的处理,减少了因资源管理不当导致的潜在风险。此优化后的代码版本仅供参考。
单线程测带宽
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/time.h>
#define BUFFER_SIZE (1024 * 1024) // 1MB buffer
#define TEST_DURATION 10 // 假定客户端也会持续发送数据同样的时长
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <server_port>\n", argv[0]);
return 1;
}
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0) {
perror("socket creation failed");
return 1;
}
int enable = 1;
if (setsockopt(server_sockfd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)) < 0) {
perror("setsockopt TCP_NODELAY failed");
close(server_sockfd);
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_sockfd);
return 1;
}
if (listen(server_sockfd, 5) < 0) {
perror("listen failed");
close(server_sockfd);
return 1;
}
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sockfd < 0) {
perror("accept failed");
close(server_sockfd);
return 1;
}
printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
char* buffer = malloc(BUFFER_SIZE);
if (!buffer) {
perror("malloc failed");
close(client_sockfd);
close(server_sockfd);
return 1;
}
// 初始化时间变量
struct timeval start, end;
gettimeofday(&start, NULL); // 先初始化但不立即使用
size_t total_received = 0;
ssize_t received;
// 等待第一次接收到数据后再开始计时
if ((received = recv(client_sockfd, buffer, BUFFER_SIZE, 0)) > 0) {
gettimeofday(&start, NULL); // 第一次接收成功后开始计时
total_received += received;
} else {
perror("recv failed during initialization");
goto cleanup;
}
while (1) {
received = recv(client_sockfd, buffer, BUFFER_SIZE, 0);
if (received <= 0) {
if (received == 0) {
printf("Client disconnected.\n");
} else {
perror("recv failed");
}
break;
}
total_received += received;
// 检查是否达到了预设的测试时长
if (gettimeofday(&end, NULL) && (end.tv_sec - start.tv_sec >= TEST_DURATION)) {
break;
}
}
cleanup:
free(buffer);
close(client_sockfd);
close(server_sockfd);
// 如果有数据接收,则计算带宽
if (total_received > 0) {
double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
double bandwidth = (total_received * 8.0) / (elapsed * 1024 * 1024); // Mbps
printf("Total data received: %zu bytes, Time: %.3f seconds, Bandwidth: %.2f Mbps\n", total_received, elapsed, bandwidth);
}
return 0;
}
这段代码实现了一个简单的TCP服务器,用于接收来自客户端的数据并计算其带宽。
- 首先检查命令行参数是否正确,如果不正确,则输出用法信息并返回。
- 创建一个套接字(服务器端)。
- 设置套接字选项TCP_NODELAY,禁用Nagle算法。
- 绑定套接字到本地地址。
- 监听套接字。
- 接受一个客户端连接请求。
- 打印客户端的地址和端口。
- 分配一个缓冲区用于接收数据。
- 初始化时间变量。
- 等待第一次接收数据成功后开始计时。
- 在循环中接收数据,直到客户端断开连接或达到预设的测试时长。
- 计算接收到的总数据量和带宽,并打印结果。
- 释放资源并关闭套接字。
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/time.h>
#define BUFFER_SIZE (1024 * 1024) // 1MB buffer
#define TEST_DURATION 10 // 测试持续时间,单位秒
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
return 1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
return 1;
}
int enable = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)) < 0) {
perror("setsockopt TCP_NODELAY failed");
close(sockfd);
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
perror("inet_pton failed");
close(sockfd);
return 1;
}
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connection failed");
close(sockfd);
return 1;
}
char* buffer = malloc(BUFFER_SIZE);
if (!buffer) {
perror("malloc failed");
close(sockfd);
return 1;
}
// 简单的带宽测试逻辑
struct timeval start, end;
gettimeofday(&start, NULL);
size_t total_sent = 0;
while (1) {
ssize_t sent = send(sockfd, buffer, BUFFER_SIZE, 0);
if (sent < 0) {
perror("send failed");
break;
}
total_sent += sent;
if (gettimeofday(&end, NULL) || (end.tv_sec - start.tv_sec >= TEST_DURATION)) {
break;
}
}
free(buffer);
close(sockfd);
// 计算带宽
double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
double bandwidth = (total_sent * 8.0) / (elapsed * 1024 * 1024); // Mbps
printf("Total data sent: %zu bytes, Time: %.3f seconds, Bandwidth: %.2f Mbps\n", total_sent, elapsed, bandwidth);
return 0;
}
编译命令:gcc -o tcp_client_single_thd tcp_client_single_thd.c
该C程序的功能是进行带宽测试,通过向指定的服务器IP和端口发送数据,并计算在一定时间内发送的数据量,从而得出带宽的值。具体实现步骤如下:
- 检查命令行参数是否为3个,如果不是,则打印使用方法并返回1。
- 创建一个套接字(TCP协议),如果创建失败则返回1。
- 设置套接字选项TCP_NODELAY,禁用Nagle算法,以减少延迟。
- 根据命令行参数设置服务器的IP地址和端口号。
- 尝试连接服务器,如果连接失败则返回1。
- 分配一个缓冲区用于发送数据,如果分配失败则返回1。
- 获取开始时间,并在一定时间内循环发送数据,直到发送失败或达到测试时长。
- 释放缓冲区,关闭套接字。
- 计算发送的总数据量、时间和带宽,并打印结果。