Linux&C语言简单使用IO多路复用(select)实现TCP并发服务器-传输层

本文详细介绍了如何通过IO多路复用(如select)技术改造TCP服务器,实现实时并发处理多个客户端连接,避免了传统TCP服务器的阻塞问题。通过服务器和客户端的代码示例,展示了如何创建监听套接字、接受连接、数据收发以及客户端交互的过程。
摘要由CSDN通过智能技术生成

IO多路复用实现TCP并发服务器-select

在这里插入图片描述

前言

服务器模型分为两种,循环服务器、并发服务器。

循环服务器: 服务器在同一时刻只能处理同一个客户端的请求。
并发服务器: 服务器在同一时刻能处理多个客户端的请求。

TCP服务器默认的就是一个循环服务器,因为内部有两个阻塞的函数, accept recv 会相互影响
UDP服务器默认的就是一个并发服务器,因为只有一个阻塞的函数 recvfrom

原理

TCP服务器有两个阻塞的函数,recv 和 accept ,相互之间会有影响,使用 io多路复用可是实现同时处理多个IO,也就是服务器能支持并发。

代码实现

服务器—01server.c

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

#define ERRLOG(errmsg)                                       \
    do                                                       \
    {                                                        \
        printf("%s--%s(%d):", __FILE__, __func__, __LINE__); \
        perror(errmsg);                                      \
        exit(-1);                                            \
    } while (0)

//创建套接字-填充服务器网络信息结构体-绑定-监听
int socket_bind_listen(const char *argv[]);

int main(int argc, const char *argv[])
{
    //检测命令行参数个数
    if (3 != argc)
    {
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }
    //创建套接字-填充服务器网络信息结构体-绑定-监听
    int sockfd = socket_bind_listen(argv);
    
    //创建文件描述符表
    fd_set readfds;//母本
    FD_ZERO(&readfds);//清空
    //每次给select使用,因为select会擦除掉一部分
    fd_set readfds_temp; //用来备份原集合的
    FD_ZERO(&readfds_temp);//清空
    //记录表中最大的文件描述符
    int max_fd = 0;
    //将要监视的文件描述符放到表中
    FD_SET(sockfd, &readfds);
    max_fd = (max_fd > sockfd ? max_fd : sockfd);//记录最大文件描述符

    int ret,ret1= 0;
    int accept_fd;
    char buff[128] = {0};
    int i;

    while (1)
    {
        // select 每次返回后 会将没有就绪的文件描述符擦除,所以
        //每次调用都需要重置这个集合
        readfds_temp = readfds;
        ret= select(max_fd + 1, &readfds_temp, NULL, NULL, NULL);
        if (ret == -1)
            ERRLOG("select error");

        //说明有文件描述符准备就绪
		// && ret > 0 是为了提高效率:
        //select返回几个就绪的,就只处理几个就绪的就行了
		//对于其他没有就绪的文件描述符 无需判断处理    
        for (i = 0; i < max_fd + 1&& ret > 0; i++)
        {
            if (FD_ISSET(i, &readfds_temp))//存在
            {//说明i就绪了
                if (i == sockfd)
                {
                    //阻塞等待客户端连接--一旦有客户端连接就会解除阻塞
                    accept_fd = accept(sockfd, NULL, NULL);
                    if (-1 == accept_fd)
                        ERRLOG("accept error");
            
                    printf("客户端 [%d] 连接了\n", accept_fd);
                    //将acceptfd也加入到要监视的集合中
                    FD_SET(accept_fd, &readfds);
                    //记录最大文件描述符
                    max_fd = (max_fd > accept_fd ? max_fd : accept_fd);
                }
                else
                {//说明有客户端发来消息了
                    accept_fd = i;
                    //接收客户端发来的数据
                    if (0 > (ret1 = recv(accept_fd, buff,sizeof(buff), 0)))
                    {
                        perror("recv error");
                        break;
                    }
                    else if (0 == ret1)//客户端CTRL+C
                    {
                        printf("客户端 [%d] 断开连接\n",accept_fd);
                        //将该客户端在集合中删除
                        FD_CLR(accept_fd, &readfds); 
                        //关闭该客户端的套接字
                        close(i);
						continue;//结束本层本次循环
                    }
                    else
                    {
                        if (0 == strcmp(buff, "quit"))
                        {
                            printf("客户端 [%d] 退出了\n",accept_fd);
                            //将该客户端在集合中删除
                            FD_CLR(accept_fd, &readfds); 
                            //关闭该客户端的套节字
                            close(i);
							continue;//结束本层本次循环
                        }

                        printf("客户端 [%d] 发来数据:[%s]\n", i, buff);
                        //组装回复给客户端的应答
                        strcat(buff, "---996");
                        //回复应答
                        if (0 > (ret1= send(accept_fd, buff,sizeof(buff), 0)))
                        {
                            perror("send error");
                            break;
                        }
                    }
                }
                ret--;
            }
        }
    }
    //关闭监听套接字  一般不关闭
    close(sockfd);
    return 0;
}

//创建套接字-填充服务器网络信息结构体-绑定-监听
int socket_bind_listen(const char *argv[])
{
    // 1.创建套接字      //IPV4   //TCP
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
        ERRLOG("socket error");


    // 2.填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));//清空
    server_addr.sin_family = AF_INET;// IPV4
    //端口号  填 8888 9999 6789 ...都可以
    // atoi字符串转换成整型数
    //htons将无符号2字节整型  主机-->网络
    server_addr.sin_port = htons(atoi(argv[2]));
    // ip地址 要么是当前Ubuntu主机的IP地址 或者
    //如果本地测试的化  使用  127.0.0.1 也可以
    //inet_addr字符串转换成32位的网络字节序二进制值
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);

    //结构体长度
    socklen_t server_addr_len = sizeof(server_addr);

    // 3.将套接字和网络信息结构体绑定
    if (-1 == bind(sockfd, (struct sockaddr *)&server_addr, server_addr_len))
        ERRLOG("bind error");
    

    //将套接字设置成被动监听状态
    if (-1 == listen(sockfd, 10))
        ERRLOG("listen error");
    
    return sockfd;
}

客户端—02client.c

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

#define ERRLOG(errmsg) do{\
		printf("%s--%s(%d):", __FILE__, __func__, __LINE__);\
		perror(errmsg);\
		exit(-1);\
	}while(0)

int main(int argc, const char *argv[])
{
	//检测命令行参数个数
	if(3 != argc){
		printf("Usage : %s <IP> <PORT>\n", argv[0]);
		exit(-1);
	}

	//1.创建套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(-1 == sockfd)
		ERRLOG("socket error");
	

	//2.填充服务器网络信息结构体
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	//端口号  填 8888 9999 6789 ...都可以
	server_addr.sin_port = htons(atoi(argv[2]));
	//ip地址 要么是当前Ubuntu主机的IP地址 或者
	//如果本地测试的化  使用  127.0.0.1 也可以
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);

	//结构体长度
	socklen_t server_addr_len = sizeof(server_addr);

	//与服务器建立连接
	if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, server_addr_len))
		ERRLOG("connect error");
	
	printf("---连接服务器成功---\n");
	char buff[128] = {0};
	while(1){
		scanf("%s", buff);
		int ret = 0;
		if(0 > (ret = send(sockfd, buff,sizeof(buff), 0)))
			ERRLOG("send error");
		
		if(0 == strcmp(buff, "quit"))
			break;
		
		if(0 > (ret = recv(sockfd, buff,sizeof(buff), 0)))
			ERRLOG("recv error");
		
		printf("收到服务器回复:[%s]\n", buff);
	}
	//关闭套接字
	close(sockfd);

	return 0;
}

执行结果

在这里插入图片描述

6.非原创

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用C语言实现IO多路复用的代码示例: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/select.h> int main() { // 创建监听套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 绑定IP地址和端口号 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(8080); bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 监听 listen(listen_sock, 5); // 创建文件描述符集合 fd_set read_fds; while(1) { // 初始化文件描述符集合 FD_ZERO(&read_fds); FD_SET(listen_sock, &read_fds); // 设置超时时间 struct timeval timeout; timeout.tv_sec = 5; timeout.tv_usec = 0; // 执行IO多路复用 int ready_num = select(listen_sock + 1, &read_fds, NULL, NULL, &timeout); if(ready_num < 0) { perror("select error"); break; } else if(ready_num == 0) { printf("timeout\n"); continue; } // 判断是否有新的连接请求 if(FD_ISSET(listen_sock, &read_fds)) { struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &client_addr_len); // 处理客户端请求 // ... close(client_sock); } } // 关闭监听套接字 close(listen_sock); return 0; } ``` 上述代码使用select函数实现IO多路复用。首先,创建了一个监听套接字,并绑定了IP地址和端口号。然后,使用select函数进行IO多路复用,设置了超时时间为5秒。在select函数返回后,通过FD_ISSET判断是否有新的连接请求,并在处理完客户端请求后关闭客户端套接字。最后,关闭监听套接字。 请注意,以上只是一个简单的示例,实际的IO多路复用代码可能会更加复杂,具体实现需要根据实际需求进行调整和完善。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [IO多路复用详解](https://blog.csdn.net/qq_54015483/article/details/130943574)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Unix C语言编写基于IO多路复用的小型并发服务器](https://blog.csdn.net/chengtang2028/article/details/100836988)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值