优秀的编程知识分享平台

网站首页 > 技术文章 正文

「Linux网络编程」TCP并发服务器的实现(IO多路复用select)

nanyue 2024-11-03 14:06:36 技术文章 9 ℃

文章目录

  • 1.2 TCP并发服务器的意义
  • 1.3 实现TCP并发服务器的方式
  • 二、使用IO多路复用实现TCP并发服务器优势
  • 四、TCP并发服务器的构建
  • 4.2 填写服务器网络信息结构体
  • 4.3 将服务器网络信息结构体与套接字绑定
  • 4.4 将套接字设置为被动监听状态
  • 4.5 创建文件描述符集合母本和子本并进行清空操作
  • 4.6 将sockfd添加进入集合内,并更新最大文件描述符
  • 4.7 循环实现内部功能伪代码
  • 5.1步骤一和二和4.1,4.2一样
  • 5.2 尝试与服务器建立连接
  • 5.3 内部功能实现伪代码
  • 七、TCP并发服务器源代码

一、服务器模型

1.1 服务器概念

服务器模型主要分为两种,循环服务器和并发服务器。

循环服务器:

在同一时间只能处理一个客户端的请求。

并发服务器:

在同一时间内能同时处理多个客户端的请求。

TCP的服务器默认的就是一个循环服务器,原因是有两个阻塞 accept函数 和recv函数 之间会相互影响。

UDP的服务器默认的就是一个并发服务器,因为只有一个阻塞的 recvfrom函数。

1.2 TCP并发服务器的意义

在有些应用场景下,我们既要 保证数据可靠 ,又要 支持并发

这就需要用到TCP并发服务器。

1.3 实现TCP并发服务器的方式

  1. 使用 多路IO复用 实现TCP并发服务器(常用)
  2. 使用 多进程 实现TCP并发服务器
  3. 使用 多线程 实现TCP并发服务器

本次我们学习第一个方式,使用 多路IO复用(select函数)实现TCP并发服务器 的实现,在后续博客中我们会依次讲解其他实现方式。

感兴趣可以收藏加关注哦。

二、使用IO多路复用实现TCP并发服务器优势

对于实际开发过程中:

如果使用多进程实现TCP并发服务器,并发量大的时候,对系统的资源占用量也会很大。

如果使用多线程,业务逻辑复杂的时候,又涉及到临近资源访问的问题

比较好的方式是使用多路IO复用实现TCP并发服务器。

三、select函数

功能:
	实现IO多路复用
头文件:
	 #include <sys/select.h>
函数原型:
	int select(int nfds, fd_set *readfds, fd_set *writefds,
               fd_set *exceptfds, struct timeval *timeout);
参数
	@nfds:监视的最大文件描述符+1
	@readfds:要监视的读文件描述符集合,如果不关心,可以传NULL
	@writefds:要监视的写文件描述符集合,如果不关心,可以传NULL
	@exceptfds:要监视的异常的文件描述符集合,如果不关心,可以传NULL
	(一般我们只关心readfds)
	@timeout:超时时间
		为0时非阻塞
		为NULL时永久阻塞
		为结构体时阻塞一定时间
返回值:
	成功  返回就绪文件描述符的个数
	失败  返回-1,置位错误码
	超时  返回0

	
void FD_CLR(int fd, fd_set *set);
功能:
	删除集合中的文件描述符
参数:
	@fd:文件描述符
	@set:构建要监视的文件描述符集合
	
int  FD_ISSET(int fd, fd_set *set);
功能:
	判断文件描述符是否在集合中
参数:
	@fd:文件描述符
	@set:构建要监视的文件描述符集合
返回值:
	为0时不在里面
	非0时在里面
	
void FD_SET(int fd, fd_set *set);
功能:
	将文件描述符添加到集合中
参数:
	@fd:文件描述符
	@set:构建要监视的文件描述符集合
	
void FD_ZERO(fd_set *set);
功能:
	清空集合
参数:
	@set:构建要监视的文件描述符集合

注意:

  1. select只能监视小于 FD_SETSIZE(1024) 的文件描述符。
  2. select函数在返回时会将没有就绪的文件描述符在表中擦除,
    所以,在循环中调用select时,每次需要重新填充集合。

四、TCP并发服务器的构建

4.1 创建套接字

使用socket函数创建IPV4、TCP套接字

int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
 
        ERRLOG("socket error");
    }

4.2 填写服务器网络信息结构体

struct sockaddr_in serviceaddr;
    memset(&serviceaddr, 0, sizeof(serviceaddr));
    serviceaddr.sin_family = AF_INET;
    serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
    serviceaddr.sin_port = htons(atoi(argv[2]));
    socklen_t serviceaddr_len = sizeof(serviceaddr);

4.3 将服务器网络信息结构体与套接字绑定

if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
    {
 
        ERRLOG("bind error");
    }

4.4 将套接字设置为被动监听状态

if (listen(sockfd, 5) == -1)
    {
 
        ERRLOG("listen error");
    }

4.5 创建文件描述符集合母本和子本并进行清空操作

fd_set readfds;
    FD_ZERO(&readfds);
    fd_set readfds_msg;
    FD_ZERO(&readfds_msg);

4.6 将sockfd添加进入集合内,并更新最大文件描述符

FD_SET(sockfd, &readfds);
    max_fd = max_fd > sockfd ? max_fd : sockfd;

4.7 循环实现内部功能伪代码

while(1){
 
        select();
        //遍历文件描述符集合
        for(){
 
            if(sockfd就绪了){
 
                //说明有新的客户端建立连接了
                acceptfd = accept();
                将acceptfd加入到readfds中
                更新最大文件描述符
            }else{
 
                //说明有客户端发来数据了
                recv();
                //如果recv返回0了 需要将当前的客户端的acceptfd在
                //readfds中删除,后续就不再监视它了
                strcat();
                send();
            }
        }
    }

五、客户端的构建

5.1步骤一和二和4.1,4.2一样

5.2 尝试与服务器建立连接

if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
 
        ERRLOG("connect error");
    }

5.3 内部功能实现伪代码

while(1){
 
        //从终端获取数据写入buff中
        fgets();
        buff[strlen(buff)-1] = '\0';//清理结尾的\n

        //发送数据
        if(-1 == send(sockfd, buff, sizeof(buff), 0)){
 
            ERRLOG("send error");
        }
        
        //接收服务器的应答信息
        if(-1 == (nbytes = recv(sockfd, buff, sizeof(buff), 0))){
 
            ERRLOG("recv error");
        }   
    }

六、测试结果

使用三个客户端连接一个TCP并发服务器。

测试TCP并发服务器功能实现

测试quit退出功能实现

测试ctrl+c终止程序断开来连接实现

测试退出文件,清除客户端的文件描述符,在新的客户端连接时,从最小的文件描述符开始实现。

成功实现IO多路复用TCP并发服务器和客户端。

七、TCP并发服务器源代码

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

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

#define N 128

int main(int argc, const char *argv[])
{
 
    //检查入参合理性
    if (argc != 3)
    {
 
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        return -1;
    }
    //创建套接字
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
 
        ERRLOG("socket error");
    }
    //填写服务器网络信息结构体
    struct sockaddr_in serviceaddr;
    memset(&serviceaddr, 0, sizeof(serviceaddr));
    serviceaddr.sin_family = AF_INET;
    serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
    serviceaddr.sin_port = htons(atoi(argv[2]));
    socklen_t serviceaddr_len = sizeof(serviceaddr);
    //将服务器网络信息结构体与套接字绑定
    if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
    {
 
        ERRLOG("bind error");
    }
    //将套接字设置为被动监听状态
    if (listen(sockfd, 5) == -1)
    {
 
        ERRLOG("listen error");
    }
    char buf[N] = {
 0};
    int max_fd;
    int i;
    int ret;
    int acceptfd;
    int nbytes;
    //创建文件描述符集合母本和子本并进行清空操作
    fd_set readfds;
    FD_ZERO(&readfds);
    fd_set readfds_msg;
    FD_ZERO(&readfds_msg);
    //将sockfd添加进入集合内,并跟新最大文件描述符
    FD_SET(sockfd, &readfds);
    max_fd = max_fd > sockfd ? max_fd : sockfd;

    while (1)
    {
 
        //在每次循环前将子本重新赋值,因为select会将没有就绪的文件描述符在集合内擦除
        readfds_msg = readfds;
        if ((ret = select(max_fd + 1, &readfds_msg, NULL, NULL, NULL)) == -1)
        {
 
            ERRLOG("select error");
        }
        else //说明有文件描述符就绪了
        {
 
            //遍历文件描述符
            for (i = 3; i < max_fd + 1 && ret != 0; i++)
            {
 
                //判断是哪个文件描述符就绪了
                if (FD_ISSET(i, &readfds_msg))
                {
 
                	ret--;
                    if (i == sockfd) //如果套接字就绪了则等待客户端连接
                    {
 
                        if ((acceptfd = accept(sockfd, NULL, NULL)) == -1)
                        {
 
                            ERRLOG("accept error");
                        }
                        printf("客户端[%d]连接到服务器..\n", acceptfd);
                        //如果有客户端连接将产生的新的文件描述符添加到集合中,并更新最大文件描述符
                        FD_SET(acceptfd, &readfds);
                        max_fd = max_fd > acceptfd ? max_fd : acceptfd;
                    }
                    else //否则就是客户端发来消息了
                    {
 
                        memset(buf, 0, N);
                        if ((nbytes = recv(i, buf, N, 0)) == -1)
                        {
 
                            ERRLOG("recv error");
                        }
                        else if (nbytes == 0)
                        {
 
                            printf("客户端[%d]已断开连接..\n", i);
                            close(i);            //关闭当前客户端的文件描述符
                            FD_CLR(i, &readfds); //将该客户端的文件描述符在集合中删除
                            continue;
                        }
                        if (strcmp(buf, "quit") == 0)
                        {
 
                            printf("客户端[%d]已退出服务器..\n", i);
                            close(i);
                            FD_CLR(i, &readfds);
                            continue;
                        }
                        printf("客户端[%d]发来消息[%s]..\n", i, buf);
                        strcat(buf, "--夜猫徐");      //组装应答
                        if (send(i, buf, N, 0) == -1) //发送给客户端
                        {
 
                            ERRLOG("send error");
                        }
                    }
                }
            }
        }
    }
    close(sockfd);
    return 0;
}

八、客户端源代码

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

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

#define N 128

int main(int argc, const char *argv[]){
 
    //入参合理性检查
    if(3 != argc){
 
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        return -1;
    }

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

    //2.填充服务器网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);

    socklen_t serveraddr_len = sizeof(serveraddr);

    char buff[128] = {
 0};
    int nbytes = 0;

    //3.尝试与服务器建立连接
    if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
 
        ERRLOG("connect error");
    }
    printf("与服务器建立连接成功..\n");
    while(1){
 
        memset(buff, 0, sizeof(buff));
        fgets(buff, N, stdin);
        buff[strlen(buff)-1] = '\0';//清理结尾的\n

        //发送数据
        if(-1 == send(sockfd, buff, sizeof(buff), 0)){
 
            ERRLOG("send error");
        }
        //接收服务器的应答信息
        if(-1 == (nbytes = recv(sockfd, buff, sizeof(buff), 0))){
 
            ERRLOG("recv error");
        }
        if(0 == nbytes){
 
            break;
        }
        //输出应答信息
        printf("应答为:[%s]\n", buff);
    }
    //关闭套接字
    close(sockfd);

    return 0;
}
最近发表
标签列表