【笔记整理 - 网络编程】

本文深入解析TCP编程的关键步骤与技巧,包括socket创建、连接管理、数据传输及错误处理等内容。涵盖重要调试方法、常见问题解决方案及多进程/多线程服务器实现。

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

重要的调试方法

查看端口号为5000的连接情况

netstat -ano | grep 5000

直接查看server程序使用的端口号及其端口号状态

netstat -anp | grep server

查看系统的一些限制

ulimit -a

查看CPU的使用情况

top

查看内存使用情况

free -m
-m:以MB为单位

主要用好socket函数。

socket提供了流(stream)和数据报(datagram)两种通信机制。

流基于TCP,数据报基于UDP。

在这里插入图片描述

代码

事后补充:Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

server.cpp
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main(int argc, char* argv[])
{

    if (argc != 2)
    {
        printf("Using:./server port\nExample:./server 5005\n\n");
        return -1;
    }

// 第1步:创建服务端的socket。
    int listenfd;
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }

// 第2步:把服务端用于通信的地址和端口绑定到socket上。
// “INADDR_ANY”表示任意ip地址,可以用带引号的ip地址指定特定ip,例如“192.168.137.128”
    sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
// servaddr.sin_addr.s_addr = inet_addr("192.168.190.134");
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(atoi(argv[1]));
    if (bind(listenfd, (sockaddr*)&servaddr, sizeof(servaddr)) != 0)
    {
        perror("bind");
        return -1;
    }

// 第3步:把socket设置为监听模式(监听模式也称为被动模式)。
    if (listen(listenfd, 5) != 0)
    {
        perror("listen");
        close(listenfd);
        return -1;
    }

// 第4步:接受客户端的连接。
    int clientfd;
    int socklen = sizeof(sockaddr_in);
    sockaddr_in clientaddr;
    // 如果不关心客户端信息,后2个参数可以直接设为“0”
    clientfd = accept(listenfd, (sockaddr*)&clientaddr, (socklen_t*)&socklen);
    printf("客户端(%s)已连接。 \n", inet_ntoa(clientaddr.sin_addr));

// 第5步:准备工作完成。与客户端通信,接收客户端发过来的报文后,回复ok。
    char buffer[1024];
    while (1)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
        {
            printf("iret = %d \n", iret);
            break;
        }
        printf("接收:%s \n", buffer);

        strcpy(buffer, "ok");
        if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
        {
            perror("send \n");
            break;
        }
        printf("发送:%s \n", buffer);
    }

    // 第6步:关闭socket,释放资源。
    close(listenfd);
    close(clientfd);
}
client.cpp
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>

int main(int argc, char* argv[])
{

    if (argc != 3)
    {
        printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");
        return -1;
    }


// 第1步:创建客户端的socket。
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }


	sockaddr_in servaddr;
// 第2步:向服务器发起连接请求。
    hostent* h;
    if ((h = gethostbyname(argv[1])) == 0)
    {
        printf("gethostbyname failed. \n");
        close(sockfd);
        return -1;
    }
        

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
    if (connect(sockfd, (sockaddr*)&servaddr, sizeof(servaddr)) != 0)
    {
        perror("connect");
        return -1;
    }

    char buffer[1024];

// 第3步:准备工作完成。与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
    for (int i = 0; i < 3; ++i)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        sprintf(buffer, "第%d条消息,编号%03d。", i + 1, i + 1);
        if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
        {
            perror("send");
            break;
        }
        printf("发送:%s \n", buffer);

        memset(buffer, 0, sizeof(buffer));
        if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
        {
            printf("iret = %d \n", iret);
            break;
        }
        printf("接收:%s \n", buffer);
    }

// 第4步:关闭socket,释放资源。
    close(sockfd);
}

socket

用于创建一个新的socket,也就是向系统申请一个socket资源。socket函数用户客户端和服务端。

int socket(int domain, int type, int protocol);

参数

domain:协议域,又称协议族(family)。

常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

《APUE》P.475

type:指定socket类型。

常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。

流式socketSOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。

数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

protocol:指定协议,通常为0,表示使用默认协议。当同一套接字组合的类型支持多个协议时,使用protocol选定一个协议。

《APUE》P.475

返回值

成功则返回一个socket,失败返回-1,错误原因存于errno 中。

实际应用中

对于使用TCP的应用,第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填0。

除非系统资源耗尽,socket函数一般不会返回失败。

!一些注意事项

标识符的分配

所有程序都会默认打开标准输入\输出\错误3个文件,所以调用socket分派到的文件标识符从3开始。

使用gdb时,socket分派到的文件标识符从7开始。

一个程序中最多能分配1024个文件标识符(0 ~ 1024)。

可以使用ulimit -a查看相关的限制。

[root@localhost coding]# ulimit -a
...
open files                      (-n) 1024
...
// 修改
[root@localhost coding]# ulimit -n 2000

标识符保留

程序结束后,原来使用的端口还有2分钟的保留时间,所以无法立即分配。(原因是TCP/IP协议的设计,具体细节忘光了)

解决方法:使用setsocket函数

int opt = 1;
unsigned int len = sizeof(opt);
setsocket(listenfd, SOL_SOCKET, SO_REUSERADDR, &opt, len);

套接字标识符的使用

套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。详情《APUE》P.476

shutdown
int shutdown(int sockfd, int how);
成功——返回0;失败——返回-1

how的取值
SHUT_RD:无法从该套接字读数据
SHUT_WR:无法向该套接字写数据
SHUT_RDWR:无法读写

意义:如果复制了一个套接字(例如使用dup()),则只有当套接字的所有活动引用关闭时,close才会释放网络端点。

shutdown允许一个套接字处于不活动状态,无论引用他的文件描述符数目。

listen accept

int listen(int sockfd, int backlog);
int accept(int sockfd,struct sockaddr* addr, socklen_t* addrlen);

自己的知识结合并不是很清晰的视频得出的总结。

服务端调用listen,将套接字设置为监听模式,然后客户端就能调用connect连接到服务端了。

在服务端调用accept前,客户端的请求都会保存在一个请求队列中,服务端调用accept从队列中取出一个请求,分配一个socket与该客户端通信。然后等待下一个accept函数调用。

所以accept函数的调用通常也是放在while循环体中。

补充!

accept函数并不是接受客户端的connect请求,而是从全连接队列拿出一个已经建立好的socket,如果队列为空,则阻塞。

https://www.cnblogs.com/pengyusong/p/6434788.html

调用accept创建了客户端套接字后,将监听套接字关闭也不会影响与客户端的通信。

三次握手与应用层的函数调用

客户端调用connect:第一次握手开始;

已调用listen的服务端:回应,第二次握手;

客户端:回应,第三次握手,connect返回。

完成握手后,服务端就能调用accept函数进行通信。

在视频后面的实践中:客户端调用connect后,服务端就会多出一个SYN_RECV状态的socket连接,当服务端调用accept后,就会有一个SYN_RECV状态的连接转换成ESTABLISHED

SYN_RECV是指,服务端被动打开后,接收到了客户端的SYN并且发送了ACK时的状态。再进一步接收到客户端的ACK就进入ESTABLISHED状态。

listen的第2个参数

https://www.cnblogs.com/ztteng/p/5147156.html

处于SYN_RECV状态的队列长度,默认值由一个配置文件决定:

listen后为accept的请求处于SYN_RECV状态。

这个值相当于设置一个瞬间能够处理的阈值。因为通常来说listen后会立即调用accept

[root@localhost ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog 
128
通过相关指令查看连接状态 netstat
// 只启动服务器
[root@localhost ~]# netstat -na | grep 5000
tcp   0   0   0.0.0.0:5000 0.0.0.0:*   LISTEN
// 用后台模式运行2次客户端
[root@localhost ~]# netstat -na | grep 5000
tcp  0  0 0.0.0.0:5000  0.0.0.0:*  LISTEN      
tcp  0  0 127.0.0.1:36636 127.0.0.1:5000   ESTABLISHED
tcp  0  0 127.0.0.1:5000  127.0.0.1:36634  ESTABLISHED
tcp  0  0 127.0.0.1:5000  127.0.0.1:36636  ESTABLISHED
tcp  0  0 127.0.0.1:36634 127.0.0.1:5000   ESTABLISHED

send recv

2者都能用write、read替代。最后一个参数目前都为0。细节参照《APUE》P.491、P.492

都有各自的缓冲区。

send的成功调用只表示数据已经被无错误地发送到网络驱动程序上。不能保证连接的另一端接收了数据。

分包和沾包

分包:一段完整的数据被拆成多段发送。

沾包:多段独立的数据被合并在一起。

视频中给出的解决方法是:在数据中加入数据长度,例如:发送“hello,world”,11个字节,所以要实际发送的数据是"0011hello,world"

讲的内容很含糊,在写代码时约定好调用send、recv的长度不好吗?

!作者自定义库的解决方案

发送方

bool TcpWrite(const int sockfd, const char* buffer, const int ibuflen = 0)
{
/* 超时机制,与IO复用有关
	if(sockfd == -1)
		return false;
		
	fd_set tmpfd;
	FD_ZERO(&tmpfd);
	FD_SET(sockfd, &tmpfd);
	
	struct timeval timeout;
	timeout.tv_sec = 5;
	timeout.tv_usec = 0;

	if(select(sockfd + 1, 0, &tmpfd, 0, &timeout) <= 0)
		return false;
*/

	int ilen = 0;
	
	(ibuflen == 0) ? ilen = strlen(buffer) : ilen = ibuflen;
	
	// 将长度转换为网络字节序
	int ilenn = htonl(ilen);
	
	char strTBuffer[ilen + 4];
	memset(strTBuffer, 0, sizeof(strTBuffer));
	// 先存入网络字节序的数据长度,再存入数据
	memcpy(strTBuffer, &ilenn, 4);
	memcpy(strTBuffer, buffer, ilen);
	
	if(Writen(sockfd, strTBuffer, ilen + 4) == false)
		return false;
		
	return true;
}

接收方(有错,应该去网站下源代码)

bool TcpRead(const int sockfd, char* buffer, int* ibuflen, const int itimeout = 0)
{
	if(sockfd == -1)
		return false;
	
	if(itimeout > 0)
	{
	
	}
	fd_set tmpfd;
	FD_ZERO(&tmpfd);
	FD_SET(sockfd, &tmpfd);
	
	struct timeval timeout;
	timeout.tv_sec = itimeout;
	timeout.tv_usec = 0;

	if(select(sockfd + 1, 0, &tmpfd, 0, &timeout) <= 0)
		return false;

	
	// 先获取数据长度
	*ibuflen = 0;
	if(Readn(sockfd, (char*)ibuflen, 4) == false)
		return false;
	*ibuflen = ntohl(*ibuflen);
	
	// 读取数据
	if(Readn(sockfd, buffer, *ibuflen) == false)
		return false;
		
	return true;
}

Writen Readn

作者封装的2个函数,分别封装了要循环调用send和recv的操作。

用户只需要将要发送、接收的一定长度的数据地址传给函数就行了,具体的在while循环体中调用sendrecv的操作由函数完成。

Writen函数的结构相同。

bool Readn(const int sockfd, const char* buffer, const size_t n)
{
	int nLeft, nread, idx;
	idx = 0;
	
	// 持续要求recv函数获取nLeft长度的数据,直到读完所有数据
	while(nLeft > 0)
	{
		if(nread = recv(sockfd, buffer + idx, nLeft, 0) <= 0)
			return false;
		idx += nread;
		nLeft -= nread;
	}
	
	return true;
}

网络中的结构体

sockaddr_in

sockaddr_in结构体是使socket代码看起来麻烦的主要原因。

不同的通信域对应不同的地址结构,sockaddr是一个通用的地址结构。

类似于多态的特性(原来是用C语言编写的,所以没有用类和继承?),sockaddr相当于一个基类,而用于网络通信的sockaddr_in和用于进程间通信的sockaddr_un就是派生类。

struct sockaddr 
{  
	sa_family_t sin_family;// 地址类型,AF_xxx
	char sa_data[14]; // 同时存放端口和ip地址
}; 

改进:sockaddr_in结构体,将ip地址与端口分开存储

struct sockaddr_in
{
	short int sin_family;			// 地址类型
	unsigned short int sin_port;	// 端口号
	struct in_addr sin_addr;		// 地址
	unsighed char in_zero[8];		// 占位,为了保持与sockaddr一样的长度
};

struct in_addr
{
	unsigned long s_addr;
}

例子

servaddr.sin_family = AF_INET;
// servaddr.sin_addr.s_addr = inet_addr(argv[1]);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(atoi(argv[2]));

端口号使用htons,地址使用htonlinet_addr

hostent

struct hostent
{
	char* h_name;		// 主机名
	char** h_aliases;	// 主机所有别名构成的字符串数组
	short h_addrtype;	// 主机IP地址类型(AF_XXX)
	short h_length;		// 驻地IP地址长度(IPV4:4,IPV6:6)
	char** h_addr_list;	// 主机的ip地址,以网络字节序存储
	#define h_addr h_addr_list[0];	
};

gethostbyname 函数

// 客户端中对地址的设置
sockaddr_in servaddr;
hostent* h;
if ((h = gethostbyname(argv[1])) == 0)
{
	printf("gethostbyname failed. \n");
	close(sockfd);
	return -1;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);

这段代码可以直接用 servaddr.sin_addr.s_addr = inet_addr(argv[1]); 替代,但只能识别ip地址,无法识别域名。

相关函数

// 将字符串IP地址转换为32位的网络字节序IP地址
// 执行失败返回0
int inet_aton(const char* cp, struct in_addr* inp);

// 将网络字节序IP地址转换成字符串IP地址
char* inet_ntoa(struct in_addr in);

// 将字符串IP地址转换成整数,与第1个函数功能一样,参数的返回方式不同
in_addr_t inet_addr(const char* cp);

!大端序小端序

上方代码中,初始化socket参数时,要考虑到大小端转换。而使用recv/send或read/write时不需要考虑,因为通讯的字节流是以1个字节(char)为单位传输的,故不需要考虑大小端转换。

大于一个字节的数据存放:

大端序:数字的高位存低位;

小端序:数字的低位存低位。

网络字节序

TCP/IP中规定好的一种数据表示格式,独立于具体的机器、操作系统等,从而保证不同主机之间能正确解释数据。

网络字节序采用大端序。

主机字节序

由具体的CPU设计决定,与OS无关。

为了实现不同主机之间的通信,要采用一种共通的字节序,这就是网络字节序。

即使是同一台机器上的2个进程,也要考虑字节序问题。(Java的JVM采用大端序)

网络字节序与主机字节序之间的转换函数:

htons()ntohs()htonl()ntohl()

host to network 、network to host

short、long

TCP协议中的主机地址和端口用整数表示

将ip地址转为二进制,将4个二进制数作为一个整体转为十进制,得到的整数就是TCP表示的地址。

192.168.190.134

11000000 10101000 10111110 10000110

3232284294

htonl(3232284294)返回的结果:

10000110 10111110 10101000 11000000

2260641984

多进程服务端

使用作者封装的CTcpServer类

重点就是fork的使用:父进程生成子进程后继续等待连接;子进程则往下执行消息处理代码。

int main()
{
    for(int i = 0; i < 10; ++i)
        signal(i, SIG_IGN);

    if(g_TcpServer.InitServer(5000) == false)
    {
        printf("服务端初始化失败,程序退出。\n");
        return -1;
    }

    while(1)
    {
        if(g_TcpServer.Accept() == false)
            continue;

// 如果是父进程调用fork,结束此次循环,进入accept阻塞
// 如果是子进程调用fork,执行下方的消息处理代码
		if(fork() > 0)
			continue;

        printf("与客户端通信的线程已创建。\n");

// 下方代码是基本上是从多线程服务器代码中的线程函数中搬来的
// 修改部分:因为使用的是多进程,所以可以直接使用g_TcpServer的成员变量保存的客户端端口号
        char strbuffer[1024];
        while(1)
        {
            memset(strbuffer, 0, sizeof(strbuffer));
            if(g_TcpServer.Recv(strbuffer,300) == false)
                break;
            printf("接收:%s \n", strbuffer);

            strcpy(strbuffer, "ok");

            strcpy(strbuffer, "ok");
            if(g_TcpServer.Send(strbuffer,300) == false)
                break;
            printf("发送:%s \n", strbuffer);
        }

        printf("客户端已断开连接。\n");

        exit(0);
    }
}

僵尸进程的处理

当一个进程终止时,OS会释放资源,但它位于进程表中的条目还存在,直到它的父进程调用wait();这是因为进程表包含了进程的退出状态。
进程已终止,但其父进程还未调用wait(),这种状态的进程被称为僵尸进程(zombie process)。所有进程终止时都会过渡到这种状态。在父进程调用了wait()后,僵尸进程的进程标识符和它在进程表中的条目都会被释放。

如果父进程没有调用wait()就终止,使得子进程称为孤儿进程(orphan process),Linux和UNIX的处理方法是: init进程定期调用wait()收集孤儿进程的退出状态,并释放最后的进程残留。

// 后面跟有“<defunct>”的就是僵尸进程
[root@localhost coding]# ps -e | grep server
  2873 pts/0    00:00:00 server
  2875 pts/0    00:00:00 server <defunct>
  2877 pts/0    00:00:00 server
[root@localhost coding]# ps -e | grep server
  2873 pts/0    00:00:00 server
  2875 pts/0    00:00:00 server <defunct>
  2877 pts/0    00:00:00 server <defunct>

解决方法:屏蔽子进程的退出信号

int main()
{
	signal(SIGCHLD, SIG_IGN);
	...
}

插入这行代码后,子进程终止就不会产生僵尸进程了

关闭多余的socket

在上述多进程服务器代码中,CTcpServer类保存有2个socker,一个是监听socket,一个是客户socket。

父进程不需要与客户端通信,子进程不需要监听操作,2者都要关闭一个socket。

解决方法:调用close函数关掉就好了。父进程关客户端socket,子进程关监听socket。

int main()
{
...
	if(fork() > 0)
	{
		close(g_TcpServer.m_clientfd);
		continue;
	}
		
	close(g_TcpServer.m_listenfd);
...
}

多进程服务器的退出和资源释放

int main()
{
	for(int i = 0; i < 100; ++i)
		signal(i, SIG_IGN);
	signal(SIGINT, ParentEXIT);
	signal(SIGTERM, ParentEXIT);
	...
	if(fork() > 0)
		continue;
		
	signal(SIGINT, ChildEXIT);
	signal(SIGTERM, ChildEXIT);
	...
}



void ParentEXIT(int sig)
{
	// 屏蔽退出信号,避免善后工作被打扰
	if(sig > 0)
	{// 除了2个终止信号,还要屏蔽自身,免得反复调用
		signal(sig, SIG_IGN);
		signal(SIGINT, SIG_IGN);
		signal(SIGTERM, SIG_IGN);
	}
	
	// 通知子进程退出
	kill(0, 15);
	
	printf("父进程退出。 \n");
	
// TCPServer类的成员函数,负责善后(释放资源、提交、回滚事务)
	TCPServer.CloseClient();	
}

// 子进程的退出处理函数就只是少了“kill(0, 15);”而已,其余完全一致

多进程服务器日志

每次服务器运行都打开一个日志文件,收发信息时都额外将信息写入作者封装号的日志文件。

通常日志文件数据开头都还带有时间。

http://www.freecplus.net/d2e2e42fa0014d04922c64b3104a0b7d.html

?增加业务逻辑

https://www.bilibili.com/video/BV11Z4y157RY?p=26

添加了用户认证功能。使用XML格式进行数据交换。

TCP短连接与长连接

短连接

连接双方只进行一次或连续多次通信,通信完成后立即断开。管理简单,不需要额外控制手段。

长连接

双方进行多次通信,通信的频率和次数不确定,需要保持这个连接。

二者没有优劣分别,根据不同场景采取不同机制。

心跳机制(保活机制)

客户端服务端通常通过心跳机制保持TCP长连接。

采用长连接的TCP连接,在连接空闲时客户端每隔一段时间(60s内,不超过120s)向服务端发送一个心跳报文,服务端也进行回复,确认连接仍生效。

如果服务端在约定时间内没有收到客户端的报文,则认为客户端已掉线,主动断开连接,释放资源。

心跳机制要程序员在应用层实现。

视频作者封装了一个Read函数,直接将超时时间设为60s。

以上是在应用层实现。

实际上TCP协议也提供了相关机制,可以通过应用层的代码进行设置。

man 7 tcp

进入tcp手册,搜索tcp_keepalive字段可以看到相关参数

tcp_keepalive_intvl ...

tcp_keepalive_probes ...

tcp_keepalive_time ...

默认配置值在/proc/sys/net/ipv4 目录下可以找到,就是一个属性对应一个文件

ls /proc/sys/net/ipv4/

这样就可以修改默认值了。

多线程服务端

出现了一堆新的线程函数。

pthread_cleanup_push();

pthread_detach();

pthread_self();

pthread_setcanceltype();

pthread_cleanup_pop();

需要另一个视频合集的知识:

https://www.bilibili.com/video/BV1zf4y1Q7Nj?p=1

退出函数(通过信号2和信号15触发)

void mainexit(int sig)
{
	// 作者的日志函数
	logfile.Write("mainexit begin. \n");
	
	// 关闭监听socket
	TcpServer.CloseListen();
	
	// vpthid是个vector容器,元素是线程id
	for(int i = 0l i < vpthid.size(); ++i)
	{
		logfile.Write("cancel %ld \n", vpthid[i]);
		pthread_cancel(vpthid[i]);
	}
	
	logfile.Write("mainexit end. \n");
	
	exit(0);
}

网络服务端性能测试

服务端性能指标是面试中必问的。

主要的性能指标:

  • 服务端并发能力(同时响应多少个连接)
  • 服务端业务处理能力(同时响应多少个业务)
  • 客户端业务响应时效()
  • 网络带宽

配合ps、top等指令。

在测试并发性能时,主要是为了测试服务端的连接能力,所以客户端业务请求不要太频繁,让服务端专注于处理建立TCP连接。

通过脚本运行多个客户端。

测试业务性能时,同类,客户端的数量不要太多。

通过修改客户端代码,在短时间内发起多次请求。

获取1秒内响应的客户端消息的数量:

grep "2021-06-02 10:02:34 接收" (日志文件目录) | wc

得到的结果就是统计日志文件中拥有"2021-06-02 10:02:34 接收"字段的行数。

IO复用

多进程/线程并发模型,是为每个socket分配一个进程/线程;

IO复用能让单个进程/线程管理多个socket。

3种I/O复用方案:select、poll、epoll

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值