测试Linux下socket每次发送数据和接受数据的最大值

本文探讨了客户端和服务端在网络编程中的一次性数据传输上限问题,并通过实验确定了本机的最大发送和接收数据量分别为384KB和64KB。此外,还提供了C语言实现的客户端和服务端代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

经试验发现,客户端一次发送数据大小有个上线,本机试验数据最大是384KB,超出最大值的时候,客户端会阻塞到发送语句哪里等待服务端接受数据。服务多也是一样,有一个最大的一次性接受数据的上线,本机试验最大接受数据是64KB,但是这个服务端好像不确定具体接受多少

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#define MAXBUF 1024 * 1024

void* recvsocket(void *arg)
{
	int st = *(int *)arg;
	char s[1024];

	while (1)
	{
		memset(s, 0, sizeof(s));
		int rc = recv(st, s, sizeof(s), 0);
		if (rc <= 0)
			break;
		printf("%s\n", s);
	}
	return NULL;
}

void* sendsocket(void *arg)
{
	int st = *(int *)arg;
	char s[1024];
	char *p = malloc(MAXBUF);//1M

	while (1)
	{
		memset(s, 0, sizeof(s));
		read(STDIN_FILENO, s, sizeof(s));
		int rc = send(st, p, MAXBUF, 0);
		printf("rc = %d\n", rc);
	}

	return NULL;
}


int main(int arg, char *args[])
{
	if (arg < 3)
	{
		return -1;
	}

	int port = atoi(args[2]);
	int st = socket(AF_INET, SOCK_STREAM, 0);//初始化socket

	struct sockaddr_in addr; // 定义一个IP地址的结构
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;// 设置结构地址类型为TCP/IP地址
	addr.sin_port = htons(port); // 制定一个端口号:8080,htons:将short类型从host字节类型转到net字节类型
	// 将字符类型的IP地址转化为int,赋给addr结构
	//addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_addr.s_addr = inet_addr(args[1]);

	//
	if (connect(st, (struct sockaddr *)&addr, sizeof(addr)) == -1)
	{
		printf("connect failed %s\n", strerror(errno));
		return EXIT_FAILURE;
	}

	pthread_t thrd1, thrd2;

	pthread_create(&thrd1, NULL, recvsocket, &st);
	pthread_create(&thrd2, NULL, sendsocket, &st);
	pthread_join(thrd1, NULL);
	//pthread_join(thrd2, NULL);// 加上这一句话会导致线程如法退出,主线程会一直等待子线程,子线程一直是退步出来的

	close(st);
	return EXIT_SUCCESS;
}


#define MAXBUF 1024 * 128

struct ps
{
	int st;
	pthread_t *thr;
};

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int status = 0;

void* recvsocket(void *arg)
{
	struct ps *p = (struct ps *)arg;

	int st = p->st;
	char s[1024];
	char *pp = malloc(MAXBUF);
	while (1)
	{
		char buf[100];
		read(STDIN_FILENO, buf, sizeof(buf));
		memset(s, 0, sizeof(s));
		int rc = recv(st, pp, MAXBUF, 0);
		printf("rc = %d\n", rc);
		if (rc <= 0)//如果recv返回小于等于0,代表socket已经关闭或者出错了
			break;
		//printf("%s\n", s);
	}
	pthread_mutex_lock(&mutex);
	status = 0;
	pthread_mutex_unlock(&mutex);
	pthread_cancel(*(p->thr));
	return NULL;
}

void* sendsocket(void *arg)
{
	int st = *(int *)arg;
	char s[1024];

	while (1)
	{
		memset(s, 0, sizeof(s));
		read(STDIN_FILENO, s, sizeof(s));
		send(st, s, strlen(s), 0);
	}

	return NULL;
}


int main(int arg, char *args[])
{
	if (arg < 2)
	{
		return -1;
	}
	int port = atoi(args[1]);
	int st = socket(AF_INET, SOCK_STREAM, 0); //初始化socket

	int on = 1;
	if (setsockopt(st, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
	{
		printf("setsockopt failed %s\n", strerror(errno));
		return EXIT_FAILURE;
	}

	struct sockaddr_in addr; // 定义一个IP地址结构
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET; // 将addr结构的属性定位为TCP/IP地址
	addr.sin_port = htons(port); // 将本地字节顺序转化为网络字节顺序
	addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY代表这个server上所有的地址

	// 将ip与server程序绑定
	if (bind(st, (struct sockaddr *) &addr, sizeof(addr)) == -1)
	{
		printf("bind failed %s\n", strerror(errno));
		return EXIT_FAILURE;
	}

	// server端开始listen
	if (listen(st, 20) == -1)
	{
		printf("listen failed %s\n", strerror(errno));
		return EXIT_FAILURE;
	}
	//printf("listen success\n");
	int client_st = 0;
	struct sockaddr_in client_addr; // 表示client端的IP地址

	pthread_t thrd1, thrd2;
	while (1)
	{
		memset(&client_addr, 0, sizeof(client_addr));
		socklen_t len = sizeof(client_addr);
		client_st = accept(st, (struct sockaddr*) &client_addr, &len);

		pthread_mutex_lock(&mutex);
		status++;
		pthread_mutex_unlock(&mutex);
		if (status > 1)
		{
			close(client_st);
			continue;
		}
		if (client_st == -1)
		{
			printf("accept failed %s\n", strerror(errno));
			return EXIT_FAILURE;
		}
		printf("accept by %s\n", inet_ntoa(client_addr.sin_addr));
		struct ps ps1;
		ps1.st = client_st;
		ps1.thr = &thrd2;
		pthread_create(&thrd1, NULL, recvsocket, &ps1);
		pthread_detach(thrd1);//设置线程为可分离
		//pthread_create(&thrd2, NULL, sendsocket, &client_st);
		//pthread_detach(thrd2);//设置线程为可分离
	}
	close(st);
	return EXIT_SUCCESS;
}



<think>我们之前已经实现了一个客户端,它每隔80ms发送一行数据,并通过检查服务器响应来避免数据堆积。现在,用户的问题集中在如何解决服务器处理速度慢导致的数据堆积问题。根据引用[1][2][3]提供的背景知识,我们可以从以下几个方面优化: 1. **网络延迟优化**:调整TCP参数(如`net.core.somaxconn``net.ipv4.tcp_max_syn_backlog`)以增加连接队列长度,减少连接请求被丢弃的情况(引用[1])。 2. **服务器架构设计**:按照功能场景划分服务器,减少进程间通信锁冲突(引用[2])。 3. **性能指标监控**:关注延时、吞吐量等指标,找出性能瓶颈(引用[3])。 4. **连接管理**:在系统层面管理多个连接,避免资源竞争(引用[4])。 针对客户端,我们可以改进之前的代码,引入更完善的流量控制机制,例如: - **滑动窗口机制**:客户端维护一个发送窗口,只有收到服务器的确认(ACK)才移动窗口并发送数据。 - **动态调整发送速率**:根据服务器的响应时间动态调整发送间隔。 由于在之前的实现中,我们已经通过等待服务器响应来避免堆积,但还不够完善(例如,在服务器响应延迟时,我们只是延长了发送间隔,但没有动态调整窗口大小)。下面我们实现一个简单的滑动窗口机制: ### 改进方案:带滑动窗口的流量控制 1. **窗口大小**:初始窗口大小为1(即每次发送一条数据,等待确认后再发送下一条)。 2. **动态调整**:如果连续收到确认,则增大窗口(例如指数增长);如果出现超时或响应慢,则减小窗口(例如减半)。 3. **定时发送**:仍然保持80ms的发送间隔,但实际发送量由窗口大小决定(即每次发送窗口内允许的多个数据包)。 然而,考虑到用户要求是每隔80ms发送一行数据,我们并不需要一次性发送多行。因此,更合适的方案是动态调整发送间隔,而不是改变每次发送数据量。 ### 改进代码:动态调整发送间隔 我们根据服务器响应时间动态调整发送间隔: - 如果服务器响应时间(RTT)较短,则保持80ms间隔。 - 如果RTT变长,则增加发送间隔(例如,将间隔调整为当前RTT的两倍,但最低不低于80ms)。 具体步骤: 1. 记录每次发送的时间戳。 2. 收到响应时,计算RTT(当前时间减去发送时间戳)。 3. 根据RTT调整下一次的发送间隔:`interval = max(80, 2 * RTT)`。这样,如果RTT为40ms,则间隔保持80ms;如果RTT为50ms,则间隔调整为100ms。 注意:如果服务器没有响应(超时),我们同样需要延长发送间隔(例如指数退避)。 ### 完整代码(改进版) ```cpp #include <iostream> #include <string> #include <chrono> #include <thread> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <cstring> #include <poll.h> #include <algorithm> // for std::max int main() { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "Socket creation error" << std::endl; return 1; } sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if (connect(sock, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "Connection failed" << std::endl; close(sock); return 1; } std::cout << "Connected to server. Starting data transmission..." << std::endl; // 设置接收超时 timeval tv; tv.tv_sec = 0; tv.tv_usec = 10000; // 10ms setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); // 初始化变量 int interval_ms = 80; // 初始间隔80ms std::chrono::steady_clock::time_point last_send_time; bool waiting_for_ack = false; long rtt_ms = 0; while (true) { auto loop_start_time = std::chrono::steady_clock::now(); // 1. 检查是否有来自服务器的ACK if (waiting_for_ack) { struct pollfd fds[1]; fds[0].fd = sock; fds[0].events = POLLIN; int poll_result = poll(fds, 1, 0); // 非阻塞检查 if (poll_result > 0 && (fds[0].revents & POLLIN)) { char buffer[1024]; int bytes_received = recv(sock, buffer, sizeof(buffer), 0); if (bytes_received > 0) { // 计算RTT(从发送到收到ACK的时间) auto ack_time = std::chrono::steady_clock::now(); rtt_ms = std::chrono::duration_cast<std::chrono::milliseconds>(ack_time - last_send_time).count(); std::string response(buffer, bytes_received); std::cout << "Server ACK: " << response << ", RTT: " << rtt_ms << "ms" << std::endl; // 根据RTT动态调整间隔:间隔 = max(80, 2 * RTT) interval_ms = std::max(80, static_cast<int>(2 * rtt_ms)); waiting_for_ack = false; } else if (bytes_received == 0) { std::cerr << "Server closed connection" << std::endl; break; } else { // 超时或错误,暂时不处理,等待下一次循环 } } } // 2. 如果当前没有等待ACK,则发送数据 if (!waiting_for_ack) { std::string data = "Data: " + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); int bytes_sent = send(sock, data.c_str(), data.size(), 0); if (bytes_sent < 0) { std::cerr << "Send error" << std::endl; break; } std::cout << "Sent: " << data << std::endl; last_send_time = std::chrono::steady_clock::now(); waiting_for_ack = true; } // 3. 计算本次循环剩余时间并休眠 auto loop_end_time = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(loop_end_time - loop_start_time); int sleep_time = interval_ms - elapsed.count(); if (sleep_time > 0) { std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); } else { // 如果处理时间已经超过间隔,则立即进入下一循环(但记录警告) std::cerr << "Warning: Processing time exceeded interval. Elapsed: " << elapsed.count() << "ms" << std::endl; } } close(sock); return 0; } ``` ### 关键改进说明 1. **动态间隔调整**:根据RTT(往返时间)调整发送间隔,公式为 $interval = \max(80, 2 \times RTT)$。这样,当RTT增加时(服务器处理变慢),发送间隔自动增大,减轻服务器负担。 2. **ACK等待机制**:发送一条数据后,等待服务器的ACK(确认)。只有收到ACK才发送下一条数据,避免数据堆积。 3. **超时处理**:设置`SO_RCVTIMEO`选项,使得`recv`调用在10ms内无数据时返回,避免永久阻塞。 4. **性能保障**:循环中计算处理时间,确保每次循环至少间隔`interval_ms`毫秒,防止发送过快。 ### 服务器端优化建议 - **调整TCP参数**:根据引用[1],调整服务器端的`net.core.somaxconn``net.ipv4.tcp_max_syn_backlog`,增加连接队列长度。 - **服务器架构**:根据引用[2],按功能或场景划分服务器,减少单点压力。 - **监控指标**:根据引用[3],监控延时、吞吐量等指标,找出瓶颈。 ### 总结 通过动态调整发送间隔等待确认机制,客户端可以自适应服务器处理速度。当服务器响应变慢时,客户端自动降低发送速率,有效防止数据堆积。同时,服务器端应进行相应优化以提升处理能力。 §§相关问题§§ 1. 如何监控TCP连接的性能指标(如RTT、吞吐量)? 2. 在服务器端如何设计高效的处理逻辑以避免成为瓶颈? 3. TCP协议中有哪些内置的流量控制机制? [^1]: 引用[1]:对于分布式C++程序,网络延迟往往是性能瓶颈的罪魁祸首。尤其是在微服务架构下,服务间频繁通信,延迟稍微高一点,整体响应时间就上去了。分析优化网络性能,得从多个角度入手。先得搞清楚延迟的来源。用测试下服务间的网络延迟,看看是网络拥塞还是路由问题。如果延迟集中在某一跳,可能是中间设备出了问题,得联系网络管理员解决。如果是整体延迟偏高,可以检查下TCP参数,比如,这些参数控制连接队列长度,默认值可能太小,导致连接请求被丢弃。调整方式是修改/etc/sysctl.conf,加上:net.core.somaxconn = 1024 net.ipv4.tcp_max_syn_backlog = 8192。 [^2]: 引用[2]:在现有的网络游戏服务器端架构中,多是以功能场景来划分服务器结构的。负载均衡集群暂且不在本文中讨论(bigworld、atlas)。服务器划分可以基于以下原则:分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。以多线程或多进程的编程方式适应多核处理器。在同一个服务器架构下,应尽可能的复用某些服务器(进程级别的复用,比如场景服务器)。运行时玩家数据的保存、修改及数据流向应该是设计的焦点,它同时也决定了服务器应该如何划分。服务器的划分应该适度,在保证清晰的数据流向的前提下,根据游戏的类型规模尽量减少服务器或服务器进程的个数,以减少服务器之间过多的复制数据、锁冲突(使用共享内存进行通讯时)。主要按照场景划分进程,若需按功能划分,必须保持整个逻辑足够简单,并满足以上1、3两点。 [^3]: 引用[3]:通常用带宽、吞吐量、延时、PPS(Packet Per Second)等指标衡量网络的性能。带宽:表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。吞吐量:表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而 吞吐量 / 带宽 ,也就是该网络的使用率。延时:表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。PPS 是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。 [^4]: 引用[4]:这就是三次握手与套接字编程的一个整体理解,三次握手以及后续四次挥手的过程中都会导致连接状态的变化,为了更好的理解其中的细节,我们需要再次将视角放的更大一些,我们从系统的角度来认识连接究竟是什么?我们要清楚在一台机器上你可能同时听着歌,看着视频,打着游戏等等一系列的网上的行为,这就意味着,我们的系统中是会同时存在多个TCP连接的,那么既然存在多个连接,我们就需要对这多个连接进行管理,怎么管理?先描述再组织,我们首先要将连接描述成一个计算机能够认识的结构化字段,比如:struct link { int sockfd; int seq; // 序号 uint16_t port; uint16_t ip; int status; // 连接状态 struct link* next; struct link* prev; //..... };</think>### C++ TCP 客户端实现:解决服务器处理速度慢导致数据堆积的机制 在 C++ TCP 实现中解决服务器处理速度慢导致的数据堆积问题,需要结合流量控制、动态速率调整服务器端优化策略。以下是基于TCP协议特性的解决方案: #### 1. **核心解决方案:客户端流量控制机制** ```cpp #include <iostream> #include <chrono> #include <thread> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <poll.h> class AdaptiveTCPClient { public: AdaptiveTCPClient(const std::string& ip, int port) : server_ip(ip), server_port(port), window_size(1), rtt_avg(80) {} void run() { while (true) { auto start = std::chrono::steady_clock::now(); // 1. 检查服务器响应(非阻塞) check_server_response(); // 2. 发送窗口允许的数据 for (int i = 0; i < window_size; ++i) { send_data(); } // 3. 动态调整发送间隔 adjust_sending_rate(start); } } private: void check_server_response() { struct pollfd pfd = {sockfd, POLLIN, 0}; int ret = poll(&pfd, 1, 10); // 10ms超时 if (ret > 0 && (pfd.revents & POLLIN)) { char ack[16]; int bytes = recv(sockfd, ack, sizeof(ack), 0); if (bytes > 0) { // 更新RTT估计值 auto now = std::chrono::steady_clock::now(); long new_rtt = std::chrono::duration_cast<std::chrono::milliseconds>( now - last_send_time).count(); rtt_avg = (rtt_avg * 7 + new_rtt) / 8; // 平滑处理 // 根据服务器响应调整窗口 adjust_window_size(); } } } void adjust_window_size() { // 动态窗口调整算法 if (rtt_avg < 100) { // 响应快,扩大窗口 window_size = std::min(window_size + 1, 10); } else if (rtt_avg > 150) { // 响应慢,缩小窗口 window_size = std::max(window_size / 2, 1); } } void adjust_sending_rate(auto start) { auto end = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); long sleep_time = 80 - elapsed.count() - (rtt_avg / window_size); if (sleep_time > 0) { std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); } } std::string server_ip; int server_port; int sockfd; int window_size; // 当前发送窗口大小 long rtt_avg; // 平均往返时间(ms) std::chrono::steady_clock::time_point last_send_time; }; ``` #### 2. **关键机制详解** 1. **动态窗口调整**: - 初始窗口大小 $w=1$(保守值) - 当RTT < 100ms 时:$w = \min(w+1, 10)$ - 当RTT > 150ms 时:$w = \max(\lfloor w/2 \rfloor, 1)$ - 通过指数加权移动平均计算RTT:$RTT_{avg} = \alpha \times RTT_{old} + (1-\alpha) \times RTT_{new}$ 2. **自适应发送间隔**: - 基础间隔:80ms - 实际间隔:$T = 80\text{ms} - \text{处理时间} - \frac{RTT_{avg}}{w}$ - 当服务器响应慢时,$RTT_{avg}$ 增大,$T$ 自动延长 3. **服务器端优化建议**: ```cpp // 服务器端应优化TCP参数(引用[1]) setsockopt(server_fd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size)); setsockopt(server_fd, SOL_TCP, TCP_NODELAY, &flag, sizeof(flag)); ``` #### 3. **数学原理分析** 客户端发送速率 $R$ 应满足: $$R \leq \frac{C}{D_s + \frac{RTT}{2}}$$ 其中: - $C$:服务器处理能力(数据包/秒) - $D_s$:服务器处理延迟(秒) - $RTT$:往返时间(秒) 当检测到 $R \times (D_s + \frac{RTT}{2}) > C$ 时,客户端应: 1. 减小发送窗口 $w$ 2. 增加发送间隔 $T$ 3. 避免数据包在服务器缓冲区堆积 #### 4. **系统级优化(引用[1][3])** 1. **调整内核参数**: ```bash # 增加TCP缓冲区 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 # 增加积压队列 net.core.somaxconn = 1024 net.ipv4.tcp_max_syn_backlog = 8192 ``` 2. **监控关键指标**: - **吞吐量**:$\text{实际数据量}/\text{时间}$ - **PPS**:$\text{包数量}/\text{时间}$ - **延迟分布**:$P_{50}, P_{90}, P_{99}$ 分位值 3. **服务器架构优化(引用[2])**: - 按功能拆分服务进程 - 共享内存减少数据复制 - 无锁队列处理网络包 #### 5. **完整工作流程** ```mermaid sequenceDiagram participant Client participant Server Client->>Server: 数据包1 (窗口大小=1) Server->>Client: ACK1 (RTT=60ms) Client->>Server: 数据包2-3 (窗口大小=2) Server->>Client: ACK2 (RTT=120ms) Client->>Server: 数据包4 (窗口大小=1) Server->>Client: ACK3 (RTT=180ms) Client->>Server: 数据包5 (窗口大小=1, 间隔+40ms) ``` #### 6. **部署注意事项** 1. 使用TCP_NODELAY禁用Nagle算法 2. 设置合理的SO_RCVTIMEOSO_SNDTIMEO 3. 监控网络指标: ```bash watch -n 1 "ss -t -i -p | grep 8080" ``` 4. 实现心跳机制检测连接状态 此方案通过动态窗口调整RTT自适应机制,在保持80ms基准间隔的同时,确保发送速率不超过服务器处理能力,有效防止数据堆积。当 $RTT > 150\text{ms}$ 时,窗口大小指数级缩减,避免服务器过载[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值