linux网络编程--shut_down和close()函数的区别

本文详细介绍了在Linux C网络编程中如何使用close和shutdown函数来关闭已连接的网络通信。通过对比这两种方法,解释了它们的工作原理及在不同场景下的适用性,特别是对于TCP连接的半双工和全双工控制。

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

Linux C网络编程中,一共有两种方法来关闭一个已经连接好的网络通信,它们就是close函数和shutdown函数,它们的函数原型分别为:

1#include<unistd.h>
2int close(intsockfd)
3//返回:0——成功, 1——失败
4 
5#include<sys/socket.h>
6int shutdown(intsockfd,inthowto)
7//返回:0——成功, 1——失败

对一个tcp socket调用close()的默认动作是将该socket标记为已关闭并立即返回到调用该api进程中。此时,从应用层来看,该socket fd不能再被进程使用,即不能再作为read或write的参数。而从传输层来看,TCP会尝试将目前send buffer中积压的数据发到链路上,然后才会发起TCP的4次挥手以彻底关闭TCP连接。
       调用close()是关闭TCP连接的正常方式,但这种方式存在两个限制,而这正是引入shutdown()的原因:
       1)close()其实只是将socket fd的引用计数减1,只有当该socket fd的引用计数减至0时,TCP传输层才会发起4次握手从而真正关闭连接。而shutdown则可以直接发起关闭连接所需的4次握手,而不用受到引用计数的限制;
       2)close()会终止TCP的双工链路。由于TCP连接的全双工特性,可能会存在这样的应用场景:local peer不会再向remote peer发送数据,而remote peer可能还有数据需要发送过来,在这种情况下,如果local peer想要通知remote peer自己不会再发送数据但还会继续收数据这个事实,用close()是不行的,而shutdown()可以完成这个任务。


close函数和shutdown函数的第一个参数都是代表的是一个文件描述符。我们知道,在linux操作系统中,一切东西都是当作文件来对待,所有的东西,诸如设备、内存都模拟成文件;当然,网络之间的通信也不例外。每一个通信对话都有一个文件描述符对应着,你对它们之间的操作就像在操作本地的文件一样。在shutdown函数当中,还有一个参数howto,它有一下三种值

  1. SHUT_RD:关闭读这一半,此时用户不能再从这个套接字读数据,这个套接口接收到的数据都会被丢弃,对等方不知道这个过程。关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。
  2. SHUT_WR:相应地关闭写这一半,此时用户不能再向套接字中写数据,内核会把缓存中的数据发送出去,接着不会再发送数据,对等端将会知道这一点。当对等端试图去读的时候,可能会发生错误。
  3. SHUT_RDWR:关闭读与写两半,此时用户不能从套接字中读或写。它相当于再次调用shutdown函数,并且一次指定SHUT_RD,一次指定SHUT_WR。

SHUT_**在函数库里面都是由宏定义的;由于shutdown提供了第二个,它可以精确的控制一个套接字描述符的关闭,这对close函数来说是无法实现的。在多线程环境中,一个描述符可能是被好几个线程复制了,它们与一个文件关联,并且内核维护一个文件引用计数,只有在引用计数为零的情况下close才可以关闭掉这个文件描述符。
使用close函数有两个限制,却可以使用shutdown来避免:

  1. close函数把描述符的引用计数减一,仅仅在该计数变为0的时候,才真正的关闭套接字,而使用shutdown函数可以不管引用计数就激发了TCP的正常连接终止序列;
  2. close函数终止读和写两个方向的数据传输。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,我们仅仅需要关闭数据发送的一个通道,但是我们还是可以接收到对端发送过来的数据,这种控制只有利用shutdown函数才能实现。
  3. 1>.如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
  4. 2>.在多进程中如果一个进程中shutdown(sfd,SHUT_RDWR)后其它的进程将无法进行通信. 如果一个进程close(sfd)将不会影响到其它进程.
转载:http://blog.youkuaiyun.com/yusiguyuan/article/details/38985271
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <pthread.h> #include <signal.h> #define PORT 8080 #define BUFFER_SIZE 1024 // 全局变量用于线程同步 volatile int running = 1; int client_socket = -1; // 线程参数结构体 typedef struct { int socket; struct sockaddr_in client_addr; } thread_args; // SIGINT信号处理函数 void handle_sigint(int sig) { running = 0; printf("\nServer shutting down...\n"); if (client_socket != -1) { shutdown(client_socket, SHUT_RDWR); close(client_socket); } exit(0); } // 接收线程函数 void* recv_thread(void* arg) { thread_args* targs = (thread_args*)arg; char buffer[BUFFER_SIZE]; while(running) { ssize_t recv_len = recv(targs->socket, buffer, BUFFER_SIZE - 1, 0); if (recv_len <= 0) { if (running) perror("Connection closed or error"); break; } buffer[recv_len] = '\0'; printf("\n[Client %s:%d]: %s> ", inet_ntoa(targs->client_addr.sin_addr), ntohs(targs->client_addr.sin_port), buffer); fflush(stdout); } running = 0; pthread_exit(NULL); } // 发送线程函数 void* send_thread(void* arg) { int socket = *((int*)arg); char buffer[BUFFER_SIZE]; printf("> "); fflush(stdout); while (running && fgets(buffer, BUFFER_SIZE, stdin) != NULL) { if (strcmp(buffer, "/exit\n") == 0) { running = 0; break; } if (send(socket, buffer, strlen(buffer), 0) < 0) { if (running) perror("Send failed"); break; } printf("> "); fflush(stdout); } running = 0; shutdown(socket, SHUT_RDWR); pthread_exit(NULL); } int main() { signal(SIGINT, handle_sigint); // 注册信号处理 int server_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len = sizeof(client_addr); // 1. 创建Socket if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR选项 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("Setsockopt failed"); close(server_fd); exit(EXIT_FAILURE); } // 2. 配置服务器地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 3. 绑定Socket到端口 if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 4. 开始监听 if (listen(server_fd, 5) < 0) { perror("Listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d...\n", PORT); printf("Press Ctrl+C to exit\n"); // 5. 接受客户端连接 if ((client_socket = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len)) < 0) { perror("Accept failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Client connected from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 创建线程参数 thread_args args = {client_socket, client_addr}; // 6. 创建收发线程 pthread_t tid_recv, tid_send; if (pthread_create(&tid_recv, NULL, recv_thread, &args) != 0) { perror("Receive thread creation failed"); close(client_socket); close(server_fd); exit(EXIT_FAILURE); } if (pthread_create(&tid_send, NULL, send_thread, &client_socket) != 0) { perror("Send thread creation failed"); close(client_socket); close(server_fd); exit(EXIT_FAILURE); } // 7. 等待线程结束 pthread_join(tid_recv, NULL); pthread_join(tid_send, NULL); close(client_socket); close(server_fd); return 0; } 如何通过select函数实现可同时读写功能
最新发布
08-08
<think>我们正在使用C语言编写一个TCP服务器,该服务器需要能够同时处理多个客户端的连接请求,并且能够同时进行读写操作(即全双工通信)。为了实现这一点,我们将使用select函数进行I/O多路复用。 步骤: 1. 创建TCP监听套接字,绑定到指定端口并开始监听。 2. 初始化一个文件描述符集合(fd_set),用于select函数监视。 3. 使用一个数组(或其他数据结构)来保存所有活动的客户端套接字,包括监听套接字。 4. 进入循环,调用select函数来监视所有套接字上的活动。 5. 当select返回时,检查哪些套接字有活动: a. 如果监听套接字有活动(有新的连接请求),则接受连接,并将新的客户端套接字加入到我们的集合数组中。 b. 对于每个客户端套接字,检查是否有数据可读(即客户端发送了数据)。如果有,读取数据并处理(例如,可以回显给客户端)。 c. 同时,我们也可以检查套接字是否可写(但通常我们只在需要写入大量数据且可能阻塞时才检查可写,对于回显服务器,我们通常可以在读取后直接写入,因为数据量小,一般不会阻塞。但如果需要,我们可以设置要发送的数据缓冲区,在可写时发送)。 6. 注意:我们需要处理客户端断开连接的情况,此时应该关闭套接字并从集合中移除。 但是,注意select函数本身可以同时监视读、写异常事件。因此,我们可以设置读集合写集合。但在这个例子中,我们主要关注读事件(包括新连接客户端数据),以及可能的写事件(如果发送缓冲区满,我们可能需要等待可写事件)。 然而,为了简化,我们通常先处理读事件,然后在有数据需要发送时,我们尝试直接发送。如果发送部分成功(或者遇到EAGAIN/EWOULDBLOCK错误),则可以将剩余数据放入缓冲区,并在下次可写时继续发送。 但在这个示例中,我们将实现一个简单的回显服务器,即读取客户端的数据并立即回显。因此,我们不会等待可写事件,因为通常TCP套接字的发送缓冲区是足够的,除非网络非常拥堵。如果遇到发送阻塞,我们可以稍作处理。 不过,为了演示同时读写,我们将使用select来监视读写事件。但请注意,对于写事件,select会告诉我们什么时候套接字的发送缓冲区有空间(即可写),这样我们就可以继续发送之前未发送完的数据。 因此,我们可能需要为每个客户端维护一个发送缓冲区。当我们需要向客户端发送数据时,如果直接调用send阻塞了(或者返回EAGAIN),我们就将剩余数据存入缓冲区,并标记该套接字需要监视可写事件。当可写事件发生时,继续发送。 但是,为了示例的简洁,我们首先实现一个不需要写事件监视的版本(即假设每次发送都能立即完成)。然后我们再扩展它,使其能够处理发送缓冲区满的情况。 由于问题要求同时读写,我们这里将实现一个更健壮的版本,包括处理发送缓冲区。 我们将分为以下步骤: 1. 创建监听套接字。 2. 初始化: - 定义读集合(read_fds)写集合(write_fds),以及一个用于所有文件描述符的数组(或列表)最大文件描述符。 - 将监听套接字加入读集合。 3. 为每个客户端维护一个发送缓冲区(例如,一个结构体数组,包含套接字发送缓冲区)。 4. 循环: a. 设置select的读集合写集合(注意每次调用select前都要重新设置,因为select会修改这些集合)。 b. 调用select。 c. 检查监听套接字是否在读集合中,如果是则接受新连接,并将新套接字加入读集合(并初始化其发送缓冲区为空)。 d. 检查每个客户端套接字: - 如果在读集合中,则读取数据。如果读取到0(客户端关闭),则关闭连接并清除相关结构;否则,将接收到的数据放入该客户端的发送缓冲区,并将该套接字加入写集合(因为我们有数据要发送)。 - 注意:我们也可以选择立即发送,但如果发送不完整,则剩余部分需要放入发送缓冲区,并加入写集合等待下次可写。 - 但是,为了统一,我们将读取数据后立即将数据放入发送缓冲区,并加入写集合。这样,我们不在读处理中直接发送,而是等待写事件来处理发送。 e. 检查每个客户端套接字是否在写集合中,如果是,则尝试发送其发送缓冲区中的数据。如果发送了一部分,则更新缓冲区(移除已发送部分)。如果全部发送完毕,则将该套接字从写集合中移除(除非我们想一直监视可写?但通常只有有数据要发时才监视)。如果发送过程中遇到错误(或连接断开),则关闭连接并清理。 5. 注意:我们还需要处理异常集合(exceptions),但通常我们只处理读写。 但是,这个逻辑稍微复杂。另一种常见的做法是:当读取到数据后,我们立即尝试发送,如果发送不完整,则将剩余数据放入发送缓冲区,并设置该套接字需要监视可写事件。然后,在可写事件发生时继续发送。 我们采用第二种做法: 步骤: - 当客户端套接字可读时,读取数据,然后尝试立即发送整个数据(即回显)。如果send函数返回的值小于要发送的字节数,说明只发送了一部分,则将剩余部分存入该客户端的发送缓冲区,并将该套接字加入写集合(以便在可写时继续发送)。 - 当客户端套接字可写时,继续发送其发送缓冲区中剩余的数据。如果发送完毕,则将其从写集合中移除(因为我们不需要再监视可写,直到下一次又有数据没发完)。 这样,我们只有在有数据未发送完时才监视可写事件,减少了select的负担。 下面我们开始编写代码框架。注意,为了简化,我们使用固定数组来存储客户端信息(包括套接字发送缓冲区),并且假设最大客户端数量。 我们定义: - 监听套接字:listen_fd - 客户端数组:client_fds[FD_SETSIZE](初始化为-1,表示空闲),同时为每个客户端维护一个发送缓冲区(结构体数组) - 读集合(readfds)写集合(writefds),以及异常集合(exceptfds,可选) - max_fd:当前最大文件描述符,用于select的第一个参数。 注意:FD_SETSIZE是系统定义的,通常是1024,表示select能处理的最大文件描述符数量。 由于我们需要为每个客户端维护发送缓冲区,我们定义如下结构体: ```c #define BUFFER_SIZE 1024 typedef struct { int fd; // 客户端套接字 char buffer[BUFFER_SIZE]; // 发送缓冲区 int buf_len; // 缓冲区中待发送数据的长度 } client_t; ``` 然后,我们创建一个client_t的数组,大小为MAX_CLIENTS(我们可以定义为FD_SETSIZE,但实际连接数可能小于这个值)。 但是,为了简单起见,我们也可以使用一个数组来存储客户端套接字,另一个数组来存储发送缓冲区。但使用结构体更清晰。 步骤: 1. 创建监听套接字,并设置为非阻塞(可选,但select本身可以处理阻塞套接字,因为select会告诉我们何时可读/可写,所以我们可以使用阻塞模式。但是,在发送数据时,如果使用阻塞模式,send可能会阻塞直到所有数据发送完,这可能会影响其他客户端。因此,我们通常将套接字设置为非阻塞,这样send在发送缓冲区满时会返回EAGAIN/EWOULDBLOCK,我们就可以将剩余数据放入缓冲区,等待可写事件。因此,我们需要将套接字设置为非阻塞。 2. 设置非阻塞函数(使用fcntl设置O_NONBLOCK)。 3. 初始化客户端数组,将所有fd初始化为-1(表示空闲),buf_len初始化为0。 4. 将监听套接字加入读集合,并记录当前最大文件描述符。 5. 循环: while (1) { // 每次循环重新设置readfdswritefds(因为select会修改它们) // 读集合:包括监听套接字所有已连接套接字(但注意:如果某个客户端已经关闭,我们就不监视了) // 写集合:只有那些发送缓冲区还有数据的客户端套接字(即buf_len>0)才加入写集合。 // 调用select // 检查监听套接字:如果可读,则accept,并将新套接字加入读集合(同时加入客户端数组,并设置为非阻塞) // 检查每个客户端套接字: // 如果可读:读取数据。如果读返回0(对端关闭)或出错(但忽略EAGAIN,因为非阻塞),则关闭连接,清理,并从读集合中移除。 // 否则,将读取到的数据放入该客户端的发送缓冲区(注意:我们这里简单地将数据复制到发送缓冲区,然后尝试发送) // 然后,我们尝试发送整个缓冲区(包括之前没发完的新数据)。如果发送不完整,则更新缓冲区(保留未发送部分),并将该套接字加入写集合(在下一轮循环中,我们会在写集合中设置它)。 // 注意:但是,我们也可以不立即发送,而是将新数据追加到发送缓冲区,然后标记需要监视可写。这样,我们统一在可写事件中发送。但这样延迟可能稍高。我们选择立即尝试发送,如果发送不完再等待可写事件。 // 所以,在可读事件中,我们读取数据后,将数据追加到发送缓冲区(注意:发送缓冲区可能还有之前没发完的数据?),然后调用send尝试发送整个发送缓冲区。如果发送了部分,则更新缓冲区(移动剩余数据到开头,并更新buf_len),然后设置该套接字需要监视可写(即下次循环时,如果还有数据,我们会将其加入写集合)。如果全部发送完,则清空缓冲区(buf_len=0),并且不需要监视可写(如果之前在写集合中,可以移除,但我们每次循环都会重新设置写集合,所以只要buf_len>0,我们就会加入写集合)。 // 但是,我们也可以选择不立即发送,而是等待写事件。为了减少延迟,我们选择立即发送。 // 然而,这里有一个问题:如果发送缓冲区满了,我们无法发送任何数据,那么send会返回-1,errno为EAGAIN。此时,我们就将新读取的数据追加到发送缓冲区,并将该套接字加入写集合(在下一轮循环中设置写集合)。 // 因此,处理读事件后的发送逻辑如下: // read_data = recv(...); // 读取数据 // if (read_data > 0) { // // 将数据追加到发送缓冲区(注意:需要检查缓冲区是否足够,这里简化,假设缓冲区足够大) // memcpy(client[i].buffer + client[i].buf_len, buf, read_data); // client[i].buf_len += read_data; // // 尝试发送整个发送缓冲区 // int sent = send(client[i].fd, client[i].buffer, client[i].buf_len, 0); // if (sent > 0) { // // 移除已发送的数据 // memmove(client[i].buffer, client[i].buffer+sent, client[i].buf_len - sent); // client[i].buf_len -= sent; // } // // 如果发送出错,且错误不是EAGAIN/EWOULDBLOCK,则关闭连接 // // 如果是EAGAIN,表示现在发送缓冲区已满,我们等待下次可写事件 // } // 但是,注意:我们可能已经在写集合中(因为之前就有未发送完的数据),所以如果发送后buf_len>0,我们仍然会在下一轮循环中将其加入写集合。 // 另一种做法:不立即发送,而是等待写事件。这样代码更统一,因为发送都在写事件中处理。但这样会增加延迟吗?可能,因为如果发送缓冲区有空闲,我们可以在读事件后立即发送,而不必等到下一次select。所以,我们采用读事件中尝试发送,如果发送不完,再等待写事件。 // 因此,我们在读事件中,将新数据追加到发送缓冲区后,立即尝试发送整个缓冲区。如果发送过程中遇到EAGAIN,我们就停止,等待写事件继续发送。 // 对于写事件: // 如果某个套接字在写集合中,说明之前发送时遇到EAGAIN(即发送缓冲区满),现在可以尝试继续发送。 // 我们对该套接字的发送缓冲区进行发送(上面一样)。如果发送成功(部分或全部),更新缓冲区。如果全部发送完,则buf_len=0,这样下一轮循环就不会将其加入写集合了。 // 注意:我们还需要处理异常事件(exceptfds),如果套接字发生异常,我们关闭连接。 } 6. 注意:在将新数据追加到发送缓冲区时,需要检查缓冲区是否溢出。这里我们使用固定大小的缓冲区,如果溢出,我们可以关闭连接(或丢弃数据),但为了简单,我们假设不会溢出(或者处理溢出情况)。 由于代码较长,我们只写关键部分。 我们使用非阻塞套接字,因此accept后需要设置非阻塞。 下面是一个简化版的代码框架(省略了错误处理细节): ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include <fcntl.h> #include <errno.h> #define PORT 8888 #define MAX_CLIENTS FD_SETSIZE #define BUFFER_SIZE 1024 typedef struct { int fd; char buffer[BUFFER_SIZE]; int buf_len; // 当前缓冲区中待发送数据的长度 } client_t; int main() { int listen_fd, new_fd, max_fd, i, nready; struct sockaddr_in server_addr, client_addr; socklen_t client_len; fd_set readfds, writefds, exceptfds; client_t clients[MAX_CLIENTS]; char buf[BUFFER_SIZE]; // 初始化客户端数组 for (i = 0; i < MAX_CLIENTS; i++) { clients[i].fd = -1; // 初始化为空闲 clients[i].buf_len = 0; } // 创建监听套接字 listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); exit(EXIT_FAILURE); } // 设置地址重用,避免端口占用 int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 绑定 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); close(listen_fd); exit(EXIT_FAILURE); } // 监听 if (listen(listen_fd, 10) < 0) { perror("listen"); close(listen_fd); exit(EXIT_FAILURE); } // 将监听套接字设置为非阻塞 int flags = fcntl(listen_fd, F_GETFL, 0); fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK); max_fd = listen_fd; // 当前最大文件描述符 FD_ZERO(&readfds); FD_SET(listen_fd, &readfds); while (1) { // 设置读集合(包括监听套接字所有客户端套接字) FD_ZERO(&readfds); FD_SET(listen_fd, &readfds); FD_ZERO(&writefds); FD_ZERO(&exceptfds); // 将监听套接字加入读集合 // 遍历客户端数组,将有效的客户端套接字加入读集合,同时如果有待发送数据(buf_len>0),则加入写集合 for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd != -1) { FD_SET(clients[i].fd, &readfds); FD_SET(clients[i].fd, &exceptfds); // 异常事件也监视 if (clients[i].buf_len > 0) { FD_SET(clients[i].fd, &writefds); } } } // 调用select nready = select(max_fd+1, &readfds, &writefds, &exceptfds, NULL); if (nready < 0) { perror("select"); continue; } // 检查监听套接字是否有新连接 if (FD_ISSET(listen_fd, &readfds)) { client_len = sizeof(client_addr); new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (new_fd < 0) { perror("accept"); } else { // 设置新套接字为非阻塞 flags = fcntl(new_fd, F_GETFL, 0); fcntl(new_fd, F_SETFL, flags | O_NONBLOCK); // 将新套接字加入客户端数组 for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd == -1) { clients[i].fd = new_fd; clients[i].buf_len = 0; // 初始化缓冲区为空 printf("New client connected, fd = %d\n", new_fd); break; } } if (i == MAX_CLIENTS) { fprintf(stderr, "Too many clients\n"); close(new_fd); } else { // 更新max_fd if (new_fd > max_fd) { max_fd = new_fd; } } } if (--nready <= 0) { continue; // 没有更多事件 } } // 检查异常事件(任何套接字有异常,我们关闭连接) for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd != -1 && FD_ISSET(clients[i].fd, &exceptfds)) { // 异常发生,关闭连接 printf("Exception on fd %d\n", clients[i].fd); close(clients[i].fd); clients[i].fd = -1; clients[i].buf_len = 0; // 注意:不再处理该套接字的读写事件 } } // 处理客户端套接字的读事件 for (i = 0; i < MAX_CLIENTS && nready > 0; i++) { if (clients[i].fd == -1) continue; int fd = clients[i].fd; // 读事件 if (FD_ISSET(fd, &readfds)) { ssize_t n = recv(fd, buf, BUFFER_SIZE, 0); if (n <= 0) { // 错误或对端关闭 if (n == 0 || errno == ECONNRESET) { printf("Client fd %d disconnected\n", fd); } else { perror("recv"); } close(fd); clients[i].fd = -1; clients[i].buf_len = 0; } else { // 将接收到的数据追加到发送缓冲区 if (clients[i].buf_len + n > BUFFER_SIZE) { // 缓冲区溢出,关闭连接(或丢弃数据,这里选择关闭) fprintf(stderr, "Send buffer overflow, closing client %d\n", fd); close(fd); clients[i].fd = -1; clients[i].buf_len = 0; } else { memcpy(clients[i].buffer + clients[i].buf_len, buf, n); clients[i].buf_len += n; // 尝试立即发送 ssize_t sent = send(fd, clients[i].buffer, clients[i].buf_len, 0); if (sent < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("send"); close(fd); clients[i].fd = -1; clients[i].buf_len = 0; } // 如果是EAGAIN,表示发送缓冲区已满,我们等待可写事件 } else { // 发送成功部分或全部 if (sent > 0) { // 移除已发送的数据 memmove(clients[i].buffer, clients[i].buffer + sent, clients[i].buf_len - sent); clients[i].buf_len -= sent; } } } } nready--; // 处理了一个事件 } // 写事件(注意:同一个套接字可能同时有读写事件,但nready已经减了读事件,写事件单独处理?) // 但是,nready是总事件数,我们这里分别处理读写,所以需要单独计数。上面的nready只减了读事件。 // 因此,我们这里不通过nready来控制循环,而是继续处理写事件(但写事件可能没有就绪?) // 实际上,我们可以在另一个循环中处理写事件,但为了简单,我们分开处理:先处理所有读事件,再处理写。 } // 处理写事件(注意:上面的循环已经处理了读事件,现在处理写事件) for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd == -1) continue; int fd = clients[i].fd; if (FD_ISSET(fd, &writefds)) { // 有写事件就绪,尝试发送缓冲区中剩余的数据 if (clients[i].buf_len > 0) { ssize_t sent = send(fd, clients[i].buffer, clients[i].buf_len, 0); if (sent < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("send"); close(fd); clients[i].fd = -1; clients[i].buf_len = 0; } } else { // 发送成功 if (sent > 0) { memmove(clients[i].buffer, clients[i].buffer + sent, clients[i].buf_len - sent); clients[i].buf_len -= sent; } } } } } } // end while(1) // 关闭监听套接字(实际不会执行到这里) close(listen_fd); return 0; } ``` 注意:上面的代码中,我们在处理读事件时,如果发送缓冲区有空间,我们尝试立即发送。在处理写事件时,我们继续发送剩余数据。这样,我们实现了同时读写。 但是,上面的代码在处理写事件时,没有减少nready(因为nready在之前处理读事件时已经减完了)。写事件的处理独立于nready,因为select返回的是所有就绪的事件(读、写、异常),而nready是总的就绪事件数。我们在处理读事件时,每处理一个读事件,nready减1,但写事件异常事件没有计入这个nready的减少。因此,我们上面的处理方式可能会漏掉一些事件(比如,如果只有写事件,那么nready>0,但读事件循环中不会处理,而写事件循环会处理,但nready在写事件中并没有减少)。但实际上,我们并不需要严格按照nready来循环,因为我们通过FD_ISSET检查了具体就绪的事件。 但是,为了效率,我们通常只处理就绪的事件。而上面的代码中,我们处理了所有客户端的读事件(通过FD_ISSET)写事件(通过FD_ISSET),不管nready。这样,如果客户端数量很大,但只有少数几个就绪,我们也会遍历所有客户端。为了优化,我们可以将就绪的客户端记录下来,但这样代码会更复杂。 另一种做法:在调用select之后,我们分别处理监听套接字、异常事件、读事件写事件,但不对nready进行递减计数,而是直接遍历所有客户端。由于FD_SETSIZE通常为1024,遍历是可以接受的。 但是,我们也可以使用一个单独的数组来记录活动的客户端(即fd不为-1的),然后遍历这个数组。上面的代码就是这样做的(遍历客户端数组)。 因此,上面的代码结构是可行的。 注意:异常事件的处理(exceptfds)在代码中已经包含,我们在循环中检查了异常事件,并关闭了连接。 这个代码是一个简单的框架,实际应用中可能需要更健壮的处理(如错误处理、资源释放等)。 最后,注意我们每次调用select之前,都需要重新设置文件描述符集合(因为select会修改它们)。 这个TCP服务器使用select实现了多路复用,支持同时读写(全双工)。 引用说明:select函数作为C语言网络编程的基础技术,虽然在一定程度上已经被新的IO多路复用技术所取代,但其在某些场景下仍然具有不可替代的优势[^1]。通过本文的深入解析,我们可以看到select函数的强大功能灵活的应用方式[^2]。</think>### 在C语言中使用select实现TCP服务器多路复用通信 下面是一个使用select函数实现TCP服务器多路复用的完整示例,支持同时读写操作: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include <fcntl.h> #include <errno.h> #define PORT 8888 #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main() { int server_fd, max_fd, activity, i, valread, sd; int client_sockets[MAX_CLIENTS] = {0}; struct sockaddr_in address; char buffer[BUFFER_SIZE] = {0}; fd_set readfds, writefds; // 创建TCP套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置套接字选项(避免地址占用) int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } // 绑定地址端口 address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 开始监听 if (listen(server_fd, 5) < 0) { perror("listen"); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); while (1) { // 初始化文件描述符集合 FD_ZERO(&readfds); FD_ZERO(&writefds); // 添加服务器套接字到读集合 FD_SET(server_fd, &readfds); max_fd = server_fd; // 添加客户端套接字到读写集合 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_sockets[i]; if (sd > 0) { FD_SET(sd, &readfds); FD_SET(sd, &writefds); // 同时监视可写状态 if (sd > max_fd) max_fd = sd; } } // 使用select监视多个文件描述符 activity = select(max_fd + 1, &readfds, &writefds, NULL, NULL); if ((activity < 0) && (errno != EINTR)) { perror("select error"); } // 检查新连接 if (FD_ISSET(server_fd, &readfds)) { int new_socket; int addrlen = sizeof(address); if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); exit(EXIT_FAILURE); } printf("New connection: socket fd=%d, IP=%s, port=%d\n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port)); // 添加新套接字到客户端数组 for (i = 0; i < MAX_CLIENTS; i++) { if (client_sockets[i] == 0) { client_sockets[i] = new_socket; printf("Adding to list of sockets as %d\n", i); break; } } } // 处理客户端IO操作 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_sockets[i]; if (FD_ISSET(sd, &readfds)) { // 处理读操作 if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) { // 客户端断开连接 getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&address); printf("Client disconnected: IP=%s, port=%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port)); close(sd); client_sockets[i] = 0; } else { // 处理接收到的数据 buffer[valread] = '\0'; printf("Received from client %d: %s\n", sd, buffer); // 准备响应数据(示例:原样返回) char* response = buffer; size_t response_len = strlen(response); // 检查套接字是否可写 if (FD_ISSET(sd, &writefds)) { // 执行写操作 if (send(sd, response, response_len, 0) != response_len) { perror("send"); } else { printf("Sent response to client %d\n", sd); } } } } } } return 0; } ``` ### 关键实现说明 1. **文件描述符集合管理** - 使用`fd_set`管理读/写文件描述符集合 - 通过`FD_ZERO`初始化集合 - 使用`FD_SET`添加需要监视的套接字 - 使用`FD_ISSET`检查就绪的套接字 2. **多路复用核心流程** ```mermaid graph TD A[初始化服务器套接字] --> B[绑定端口并监听] B --> C[初始化客户端数组] C --> D[设置读/写文件描述符集合] D --> E[调用select监视IO事件] E --> F{有新连接?} F -->|是| G[接受新客户端] F -->|否| H{有可读客户端?} H -->|是| I[读取客户端数据] I --> J{有可写状态?} J -->|是| K[发送响应数据] J -->|否| L[等待下次循环] H -->|否| M{有可写客户端?} M -->|是| N[发送待处理数据] ``` 3. **同时读写处理** - 读操作:当客户端发送数据时,`select`会通过读集合通知服务器 - 写操作:当TCP发送缓冲区可用时,`select`会通过写集合通知服务器 - 读写分离:独立处理读/写事件,避免阻塞 4. **性能优化点** - 设置`SO_REUSEADDR`避免地址占用 - 使用非阻塞IO模式(可添加`fcntl(sd, F_SETFL, O_NONBLOCK)`) - 限制最大客户端数量防止资源耗尽 - 使用缓冲区管理待发送数据 ### 测试方法 1. 编译代码:`gcc server.c -o server` 2. 运行服务器:`./server` 3. 使用telnet测试:`telnet localhost 8888` 4. 发送测试数据,观察服务器响应 此实现展示了select函数如何同时监视多个文件描述符的读写状态,实现真正的全双工通信。select作为基础的多路复用技术,虽然在高并发场景下有效,但在处理海量连接时,epoll或kqueue等现代技术通常性能更佳[^1][^2]。 --- ### 相关问题 1. **select函数有哪些主要局限性?** select的主要限制包括文件描述符数量上限(通常1024)、每次调用需要重新设置文件描述符集合、线性扫描所有描述符效率低等[^1][^3]。 2. **如何处理select函数中的EINTR错误?** 当select被信号中断时,应检查errno是否为EINTR并重新调用select,确保程序不会意外终止。 3. **select与epoll的主要区别是什么?** epoll使用事件驱动模型,无需每次重新传递描述符集合,支持边缘触发模式,且能处理更大规模的并发连接[^1]。 4. **如何扩展此服务器支持更多客户端?** 可改用epoll实现,增加线程池处理IO,优化缓冲区管理,或使用更高效的数据结构存储客户端信息[^2]。 [^1]: select函数作为C语言网络编程的基础技术,虽然在一定程度上已经被新的IO多路复用技术所取代,但其在某些场景下仍然具有不可替代的优势。 [^2]: select函数允许程序同时监视多个文件描述符(sockets、stdin、stdout等),并在其中任何一个文件描述符准备好进行IO操作(如读或写)时通知程序。 [^3]: 程序中使用了select函数来实现I/O多路复用,同时使用了fd_set类型来存储多个文件描述符。当某个文件描述符上有可读事件时,select会返回,然后程序会对所有处于读就绪状态的文件描述符进行处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值