网络编程篇二(实现HTTP请求器&搭建TCP服务器)

服务器 0

目录

一、前言

二、基础知识

三、HTTP请求器(TCP客户端)

3.1.HTTP工作原理

3.2.客户端请求消息 

3.3.实现HTTP请求器实例

四、TCP服务器

4.1.TCP客户端/服务端开发流程

4.2.I/O多路复用机制

4.2.1.select、poll和epoll的工作原理

4.2.2.触发方式

4.2.3.阻塞和非阻塞的区别

4.3.TCP服务器实例


一、前言

        上文网络编程篇一中的DNS请求是基于UDP通信协议实现的,而本文要实现的HTTP请求器是基于TCP协议实现的。UDP (User Datagram Protocol) 是一种无连接的通信协议,它不保证数据的可靠传输,但是传输速度较快。UDP适用于实时性要求较高的应用,如视频、音频传输等。而TCP (Transmission Control Protocol) 是一种面向连接的通信协议,它保证数据的可靠传输,但是传输速度相对较慢。TCP适用于要求可靠性的应用,如HTTP、FTP等。在本文中,由于进行HTTP请求需要确保数据的可靠传输,因此选择使用TCP协议来实现HTTP请求器。这样可以保证在与服务器进行通信时的数据传输的可靠性和稳定性。        

二、基础知识

1.HTTP

        HTTP(HyperText Transfer Protocol)是一种用于传输超文本数据的应用层协议,它是在Web浏览器和Web服务器之间进行通信的基础。HTTP使用TCP/IP协议作为传输协议,通过可靠的连接来传输数据。

        通俗来讲,HTTP是一个在计算机世界里专门在两点之间传递文字、图片、音视频等超文本数据的约定和规范。

HTTP优缺点:

优点:

  • 简单易用:HTTP协议设计简单,容易理解和使用,使得开发者能够快速构建Web应用程序。
  • 跨平台:HTTP是一种无状态的协议,不受操作系统和硬件平台的限制,可以在不同的系统上进行通信。
  • 灵活可扩展:HTTP协议可以通过添加新的头部字段来扩展其功能,允许开发者进行自定义和灵活的协议扩展。

缺点:

  • 无状态:HTTP协议是无状态的,即服务器无法跟踪客户端的状态信息。对于有状态的应用程序来说,需要使用额外的机制来维护状态信息。
  • 安全性较低:HTTP协议的数据传输是明文的,不提供加密和身份验证机制,容易被恶意攻击者截取和篡改数据。为了提高安全性,通常需要使用HTTPS协议进行加密通信。

2.HTTPs

        HTTPs(HTTP Secure)是在HTTP的基础上添加了安全性的协议,它使用了SSL/TLS协议对HTTP的数据进行加密。通过使用证书和公钥加密技术,HTTPs可以提供对数据的加密和身份验证,从而保护用户隐私和数据的安全性。

        相对于HTTP,HTTPs在传输过程中增加了数据的保密性和完整性。它可以防止数据被窃听、篡改和伪造,并且可以验证服务器的身份是否可信。常见的使用HTTPs的场景包括网上银行、电子商务、用户登录等需要保护用户隐私和数据安全的应用。

3.UDP与TCP的区别

        UDP和TCP是两种不同的传输层协议,用于在计算机网络中进行数据通信。他们的区别如下:

  • 连接性:TCP是一种面向连接的协议,它要求在数据传输之前建立一个连接,然后在连接上可靠地传输数据;UDP是一种无连接的协议,它不需要连接,只是简单地将数据包发送到目标。
  • 可靠性:TCP提供可靠性传输,确保数据包的完整性和顺序性。如数据包损坏或丢失,会重新传输;UDP不提供可靠性保证。
  • 开销:TCP具有较高的开销,UDP开销较小。
  • 适用场景:TCP适用于那些需要可靠性和数据完整性的应用,如网页浏览、电子邮件和文件下载;UDP适用于那些对延迟更为敏感的应用,如音频和视频流媒体、在线游戏以及一些实时通信应用。

4.TCP的三次握手,四次挥手?

        三次握手是指TCP建立连接的过程,四次握手是指TCP终止连接的过程。握手指客户端和服务端的交互。

TCP的三次握手是为了确保双方建立可靠的通信连接。简单描述如下:

  1. 第一次握手:客户端向服务器发送SYN包,表示请求建立连接。
  2. 第二次握手:服务器收到SYN包后,会发送一个SYN+ACK包作为确认,并同时向客户端发送一个SYN包。
  3. 第三次握手:客户端收到服务器的SYN+ACK包后,会发送一个ACK包作为确认。此时,连接已建立。

TCP的四次挥手是为了确保双方能够正常关闭连接。简单描述如下:

  1. 第一次挥手:客户端发送一个FIN包,表示自己已经不再发送数据。
  2. 第二次挥手:服务器收到FIN包后,会发送一个ACK包作为确认,并进入CLOSE_WAIT状态。
  3. 第三次挥手:服务器发送一个FIN包,表示服务器也不再发送数据。
  4. 第四次挥手:客户端收到服务器的FIN包后,会发送一个ACK包作为确认,并进入TIME_WAIT状态。服务器收到ACK包后,双方的连接正式关闭。

5.OSI模型

        OSI模型是控制计算机和网络设备之间信息交换的标准框架,全称为开放系统互联通信参考模型(Open Systems Interconnection Reference Model)。它于1984年由国际标准化组织(ISO)提出,并被广泛接受和应用。

        OSI模型将网络通信过程划分为七个不同的层次,每个层次都有特定的功能和任务。这些层次依次是:

  1. 物理层
    • 功能:物理层是OSI模型的最底层,它定义了接口和媒体的物理特性,包括数据传输速率、信号传输模式(单工、半双工、全双工)以及网络物理拓扑(网状、星型、总线型等)。物理层为设备之间的数据通信提供传输媒体及互连设备,如架空明线、平衡电缆、光纤、无线信道等。
    • 作用:为数据通信提供物理连接,确保数据能在物理介质上正确传输。
  2. 数据链路层
    • 功能:数据链路层负责在物理层提供的服务基础上,将数据封装成帧,并通过物理层进行传输。它还包括帧定界、帧同步、差错检测与恢复、流量控制等功能。
    • 作用:为网络层提供可靠的数据传输服务,确保数据帧能够准确无误地从源节点传输到目的节点。
  3. 网络层
    • 功能:网络层负责将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方。它还包括路由选择、中继、差错检测与恢复、排序、流量控制等功能。
    • 作用:实现网络之间的互连,确保数据能够跨越多个网络进行传输。
  4. 传输层
    • 功能:传输层是端到端的层次,负责建立、维护和终止端到端的连接,确保数据可靠、顺序、无错地从源端传输到目的端。它还包括流量控制、差错控制等功能。
    • 作用:为上层应用提供可靠的数据传输服务,确保数据在传输过程中不会丢失或出错。
  5. 会话层
    • 功能:会话层负责在网络中的两个节点之间建立和维持通信会话,控制会话的建立、同步和管理等。
    • 作用:为表示层提供会话服务,确保两个节点之间的通信能够顺利进行。
  6. 表示层
    • 功能:表示层负责数据的编码、解码、加密、解密、压缩和解压缩等,以确保数据能够在不同的系统之间正确传输和解析。
    • 作用:为应用层提供数据表示服务,确保数据在传输过程中能够保持其原有的格式和含义。
  7. 应用层
    • 功能:应用层是OSI模型的最高层,它为用户和应用程序提供网络服务接口,如文件传输、电子邮件、远程登录等。
    • 作用:为用户和应用程序提供直接的网络服务支持,确保用户能够方便地使用网络资源。

三、HTTP请求器(TCP客户端)

3.1.HTTP工作原理

        HTTP 协议工作于客户端-服务端架构上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求。常见的Web 服务器有:Apache 服务器,IIS 服务器(Internet Information Services)等。Web 服务器根据接收到的请求后,向客户端发送响应信息。
        HTTP 默认端口号为 80,但是你也可以改为 8080 或者其他端口。

HTTP 三点注意事项:

  • HTTP 是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • HTTP 是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过 HTTP 发送。客户端以及服务器指定使用适合的 MIME-type 内容类型。
  • HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。 

3.2.客户端请求消息 

http请求报文格式:客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

下面实例是一点典型的使用 GET 来传递数据的实例:
客户端请求:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

根据 HTTP 标准,HTTP 请求可以使用多种请求方法 

  1. GET:用于获取资源,可以理解为读取或下载数据。适用于向服务器请求资源,在URL上携带数据的长度有限制。
  2. HEAD:类似于GET请求,但服务器仅返回响应头部信息,不返回实际的资源内容。常用于检查资源是否存在,或获取资源的元数据。
  3. POST:向服务器提交数据,相当于写入或上传数据。通常用于发送表单数据、上传文件等场景。
  4. PUT:用于向服务器上传或更新资源。通常用于创建新资源或覆盖已存在的资源,在请求中携带完整的资源内容。
  5. DELETE:请求服务器删除指定的资源。
  6. OPTIONS:用于获取服务器支持的请求方法列表。

 http响应报文格式:HTTP 响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

HTTP状态码
        当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并
显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求。
下面是常见的 HTTP 状态码:

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它 URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误 

3.3.实现HTTP请求器实例

这段代码实现了一个简单的HTTP客户端,可以向指定的主机发送HTTP请求并接收响应。

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <netdb.h>#include <fcntl.h>#define HTTP_VERSION		"HTTP/1.1"#define CONNETION_TYPE		"Connection: close/r/n"#define BUFFER_SIZE		4096// DNS --> // baidu --> struct hostenchar *host_to_ip(const char *hostname) {	struct hostent *host_entry = gethostbyname(hostname); //dns	// 14.215.177.39 --> 	//inet_ntoa ( unsigned int --> char *	// 0x12121212 --> "18.18.18.18"	if (host_entry) {		return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list);	} 	return NULL;}int http_create_socket(char *ip) {	int sockfd = socket(AF_INET, SOCK_STREAM, 0);	struct sockaddr_in sin = {0};	sin.sin_family = AF_INET;	sin.sin_port = htons(80); // 	sin.sin_addr.s_addr = inet_addr(ip);	if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {		return -1;	}	fcntl(sockfd, F_SETFL, O_NONBLOCK);	return sockfd;}// hostname : github.com --> char * http_send_request(const char *hostname, const char *resource) {	char *ip = host_to_ip(hostname); // 	int sockfd = http_create_socket(ip);	char buffer[BUFFER_SIZE] = {0};	sprintf(buffer, "GET %s %s/r/n/Host: %s/r/n/%s/r/n//r/n",	resource, HTTP_VERSION,	hostname,	CONNETION_TYPE	);	send(sockfd, buffer, strlen(buffer), 0);		//select 	fd_set fdread;		FD_ZERO(&fdread);	FD_SET(sockfd, &fdread);	struct timeval tv;	tv.tv_sec = 5;	tv.tv_usec = 0;	char *result = malloc(sizeof(int));	memset(result, 0, sizeof(int));		while (1) {		int selection = select(sockfd+1, &fdread, NULL, NULL, &tv);		if (!selection || !FD_ISSET(sockfd, &fdread)) {			break;		} else {			memset(buffer, 0, BUFFER_SIZE);			int len = recv(sockfd, buffer, BUFFER_SIZE, 0);			if (len == 0) { // disconnect				break;			}			result = realloc(result, (strlen(result) + len + 1) * sizeof(char));			strncat(result, buffer, len);		}	}	return result;}int main(int argc, char *argv[]) {	if (argc < 3) return -1;	char *response = http_send_request(argv[1], argv[2]);	printf("response : %s/n", response);	free(response);	}

下面是对代码的简要解释:

  1. host_to_ip函数:该函数使用gethostbyname函数将主机名转换为对应的IP地址。

  2. http_create_socket函数:该函数创建一个套接字,并使用connect函数连接到指定的IP地址和端口号(80)。

  3. http_send_request函数:该函数接收主机名和资源路径作为参数,首先调用host_to_ip函数获取对应的IP地址,然后调用http_create_socket函数创建套接字并连接到主机。接下来,根据HTTP协议的格式构建HTTP请求报文,使用send函数将请求发送到服务器。之后,通过使用select函数来轮询套接字是否有数据可读,如果有可读数据,则使用recv函数读取数据并将其存储在缓冲区中,并将缓冲区的内容追加到结果字符串中。最后,返回结果字符串。

  4. main函数:该函数通过命令行参数接收主机名和资源路径,并调用http_send_request函数发送HTTP请求并将响应打印出来。

疑问?上面代码中使用的select是什么?有什么用?

回答:

        select() 是一个用于多路复用 I/O 的函数。它可以同时监视多个文件描述符,一旦其中一个或多个文件描述符准备好进行 I/O 操作(可读、可写、出错等),select() 函数就会返回。这样可以避免在没有数据可读或写入时阻塞程序。

        如果不使用select,而是直接使用阻塞式的recv函数,那么在每次接收数据时都需要等待服务器返回数据,如果服务器的响应时间较长,那么程序会一直阻塞在recv函数的调用处,无法进行其他的操作。

        使用select函数可以设置一个超时时间,可以在超时时间内检测socket是否有数据可读,如果没有数据则可以进行其他的操作,避免了阻塞。

        综上所述,使用select可以提高程序的并发性能和响应速度,提高了代码的可扩展性。

四、TCP服务器

4.1.TCP客户端/服务端开发流程

TCP客户端程序开发流程

  1. 创建客户端套接字对象。socket();
  2. 与服务端套接字建立连接。connect();
  3. 发送数据。send();
  4. 接收数据。recv();
  5. 关闭客户端套接字.close().

TCP服务端开发流程:

  1. 创建服务端套接字对象。socket();
  2. 绑定IP地址和端口号。bind();
  3. 设置监听。listen();
  4. 等待接收客户端的连接请求。accept();
  5. 接收数据。recv();
  6. 发送数据。send();
  7. 关闭服务端套接字。close()。

4.2.I/O多路复用机制

        在前面的tcp客户端程序中,在使用recv()函数接收数据时使用了I/O多路复用机制——select。这样避免了使用recv()接收不到数据而出现阻塞,select可以设置超时时间,一段时间未收到数据就会结束该段程序。下面将详细介绍几种常用的I/O复用机制,select、poll和epoll。

4.2.1.select、poll和epoll的工作原理

select:

  • 原理:select是最早出现的I/O多路复用机制,可以同时监视多个I/O事件。它通过将需要检测的文件描述符集合传递给select函数,并通过轮询的方式来检测是否有事件发生。当有事件发生时,select函数会返回,程序可以根据返回值进行相应的处理。
  • 优点:单线程处理多个连接、避免阻塞、较低资源消耗;
  • 缺点:参数较多;效率低,每次需要把待检测文件描述符集合copy进入内核,然后不断轮询;可以监听的文件描述符数量有限,通常为1024。
  • 实现函数:int nready=select(maxfd,rset,wset,eset,timeout)。

poll:

  • 原理:poll是select的改进版,使用方式类似。与select相比,poll的一个优势是没有文件描述符的数量限制,可以处理更多的连接。poll通过将需要监视的文件描述符数组传递给poll函数,并通过轮询的方式来检测是否有事件发生。
  • 优点:无文件描述符数量的限制;相比于select参数更少。
  • 缺点:效率低,每次需要把待检测文件描述符集合copy进入内核,然后不断轮询;
  • 实现函数:int nready = poll(fds,maxfd+1,-1)。

epoll:

  • 原理:epoll是Linux系统下的I/O多路复用机制,相比于select和poll,具有更高的性能。epoll通过将需要监视的文件描述符加入到监听队列中,当有事件发生时,只通知发生事件的文件描述符。这种方式避免了轮询的开销,提高了效率。
  • 优点:

                1.事件驱动,效率高,具有较低的系统调用开销。

                2.高并发,支持较大的并发连接数,可以监听上万个文件描述符。

                3.具有较好的可移植性,适用于大部分操作系统。

  • 缺点:

                1.只能在Linux系统下使用:Epoll是Linux内核中的一个特性,因此只能在Linux系统下使用。

                2.编程接口相对复杂,使用起来相对困难一些。

  • 实现函数:epoll_create()、epoll_ctl()、epoll_wait()。

 

疑问:epoll相比于select、poll的优势是什么?

回答:

  1. 高效:epoll使用内核事件通知机制,能够在大量的文件描述符中高效地找出可读、可写或可异常事件,并且避免了遍历所有文件描述符的开销。
  2. 支持边缘触发:epoll提供了边缘触发模式(edge-triggered),即只有在状态变化时才得到通知。这意味着当一个事件发生后,应用程序必须立即进行处理,否则事件会被丢弃。
  3. 内核与用户空间共享事件表:使用epoll时,内核会将事件信息填充到用户空间中的事件表中,减少了内核和用户空间之间的数据拷贝操作,提高了效率。
  4. 支持多线程。

4.2.2.触发方式

        两种触发方式是边沿触发(Edge Triggered,ET)和水平触发(Level Triggered,LT)。

1.ET边沿触发:

  • ET模式下,只有在触发事件时才会通知应用程序,再次进行非阻塞I/O操作。
  • 当数据就绪时,epoll_wait函数会返回,并且应用程序需要一次性将所有就绪的数据读取完毕,否则会有数据丢失的风险。
  • ET触发模式较为高效,适用于高并发的情况,但需要应用程序具备高并发处理的能力。

2.LT水平触发:

  • LT模式下,当数据就绪时,epoll_wait函数会返回,并且应用程序可以立即进行非阻塞I/O操作。
  • 如果应用程序没有完全读取所有就绪的数据,下次epoll_wait返回时还会再次触发事件,直到数据全部读取完毕。
  • LT触发模式相对于ET触发模式更容易使用,但在高并发情况下,效率可能略低。

总结:

  • ET触发方式适用于高并发情况下,要求应用程序有较高的并发处理能力。
  • LT触发方式适用于一般情况下,使用更加简单方便。
  • 选择哪种触发方式应根据实际情况和需求来决定。

4.2.3.阻塞和非阻塞的区别

        阻塞和非阻塞是指线程或进程在执行某个操作时的行为方式。

阻塞:当一个线程或进程执行某个操作时,如果操作不能立即完成,那么线程或进程将会被挂起,等待操作完成后再继续执行后续任务。在这个等待的过程中,该线程或进程无法执行其他任务。

非阻塞:当一个线程或进程执行某个操作时,如果操作不能立即完成,线程或进程不会被挂起,而是立即返回,继续执行后续任务。在这个过程中,该线程或进程可以同时处理其他任务。

        在高并发编程中,阻塞方式可能导致线程或进程的资源浪费,因为线程或进程被挂起时无法做其他事情。而非阻塞方式则可以提高系统的并发能力,充分利用线程或进程的资源。因此,非阻塞方式在高并发场景中更加常用。

4.3.TCP服务器实例

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <netinet/tcp.h>#include <arpa/inet.h>#include <pthread.h>#include <errno.h>#include <fcntl.h>#include <sys/epoll.h>#define BUFFER_LENGTH		1024#define EPOLL_SIZE			1024void *client_routine(void *arg) {	int clientfd = *(int *)arg;	while (1) {		char buffer[BUFFER_LENGTH] = {0};		int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);		if (len < 0) {			close(clientfd);			break;		} else if (len == 0) { // disconnect			close(clientfd);			break;		} else {			printf("Recv: %s, %d byte(s)/n", buffer, len);		}	}}// ./tcp_server int main(int argc, char *argv[]) {	if (argc < 2) {		printf("Param Error/n");		return -1;	}	int port = atoi(argv[1]);	int sockfd = socket(AF_INET, SOCK_STREAM, 0);	struct sockaddr_in addr;	memset(&addr, 0, sizeof(struct sockaddr_in));	addr.sin_family = AF_INET;	addr.sin_port = htons(port);	addr.sin_addr.s_addr = INADDR_ANY; 	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {		perror("bind");		return 2;	}	if (listen(sockfd, 5) < 0) {		perror("listen");		return 3;	}	// #if 0	while (1) {		struct sockaddr_in client_addr;		memset(&client_addr, 0, sizeof(struct sockaddr_in));		socklen_t client_len = sizeof(client_addr);		int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);				pthread_t thread_id;		pthread_create(&thread_id, NULL, client_routine, &clientfd);	}	#else	int epfd = epoll_create(1);  	struct epoll_event events[EPOLL_SIZE] = {0};	struct epoll_event ev;	ev.events = EPOLLIN; 	ev.data.fd = sockfd;	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);		while (1) {		int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5); // -1, 0, 5		if (nready == -1) continue;		int i = 0;		for (i = 0;i < nready;i ++) {			if (events[i].data.fd == sockfd) { // listen 				struct sockaddr_in client_addr;				memset(&client_addr, 0, sizeof(struct sockaddr_in));				socklen_t client_len = sizeof(client_addr);				int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);				ev.events = EPOLLIN | EPOLLET; 				ev.data.fd = clientfd;				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);			} else {				int clientfd = events[i].data.fd;								char buffer[BUFFER_LENGTH] = {0};				int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);				if (len < 0) {					close(clientfd);					ev.events = EPOLLIN; 					ev.data.fd = clientfd;					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);									} else if (len == 0) { // disconnect					close(clientfd);					ev.events = EPOLLIN; 					ev.data.fd = clientfd;					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);									} else {					printf("Recv: %s, %d byte(s)/n", buffer, len);				}											}		}	}	#endif		return 0;}

 代码讲解:

        这段代码是一个简单的TCP服务器程序。它首先创建一个套接字sockfd,并将其绑定到指定的端口上。然后通过调用listen函数将该套接字设置为监听状态,等待客户端的连接。

        在传统的实现中,使用了多线程来处理每个客户端连接。当有新的连接到达时,会创建一个新的线程来处理该连接。这部分代码被注释掉了,暂时不考虑。

        新的实现中,使用了epoll来处理客户端连接。首先创建了一个epoll实例epfd,并定义了一个epoll_event类型的数组events。然后将sockfd添加到epoll实例中,监听读事件(EPOLLIN)。

        进入主循环,调用epoll_wait函数等待事件发生,最多等待5秒。当事件发生时,会返回就绪的事件数量nready。接下来就是遍历就绪事件的过程。

        如果就绪事件是sockfd(监听事件),说明有新的客户端连接到达。通过accept函数接收新的客户端连接,并将该连接的文件描述符添加到epoll实例中,监听读事件(EPOLLIN | EPOLLET)。

        如果就绪事件是客户端连接的文件描述符,说明有数据可读。通过recv函数读取数据,并进行处理。如果读取失败或者读取到了0字节,说明连接已断开,将该连接的文件描述符从epoll实例中删除。

        整个程序的主要思路就是通过epoll来监听多个文件描述符的读事件,实现并发处理多个客户端连接。

也许您对下面的内容还感兴趣: