网络io与select

我们知道网络IO模型一共有5种,这里我们主要讨论同步IO和select多路复用的情况。
我们先从一个简单的TCP服务器的代码出发,来讨论一下这个是怎么实现的。

一个十分简单的TCP服务器

一个简单的TCP的服务器的建立流程是这样

  • 建立SOCKET
  • 绑定端口
  • 监听
  • 接受连接
  • 接受消息
  • 发送消息
  • 关闭连接
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>



#define BUFFER_LENGTH 128


int main()
{
    //socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
    //这里使用0,表示让系统自动推导我们需要使用的传输协议。
    int listenfd= socket(AF_INET,SOCK_STREAM,0);
    //如果返回值为-1,说明我们创建SOCKET失败,直接返回。
    if (listenfd==-1)
    {
        return -1;
    }
    //我们需要绑定的信息
    struct sockaddr_in serveraddr;
    //使用IPV4
    serveraddr.sin_family=AF_INET;
    //我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //我们需要绑定的端口,这里我们绑定的端口为9999
    serveraddr.sin_port=htons(9999);
    //第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。    
    if (-1==bind(listenfd,(const  sockaddr*)&serveraddr,sizeof(serveraddr)))
    {
        return -2;        
    }
    //监听我们创建的套接字,请求的队列数量,这里我们填写为10个
    listen(listenfd,10);
    //定义客户端的socket
    struct sockaddr_in client;
    //客户端结构体的长度
    socklen_t len=sizeof(client);
    //等待接受连接
    //第一个参数服务器的套接字,第二个接收到的客户端的socket,第三个函数就是结构体的长度
    int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
    //接受的缓冲区大小
    unsigned char buffer[BUFFER_LENGTH]={0};
    //收函数
    //第一个参数客户端的套接字,第二个参数,接受的缓冲区,第三个参数缓冲区的大小,第4个参数接收到的字节数
    int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
    if (ret==0)
    {
       close(clientfd);

    }
    printf("buffer: %s , ret : %d\n",buffer,ret);
    //发函数
    //第一个参数客户端的套接字,第二个参数,发送的缓冲区,第三个参数发送的字节数,第4个参数实际发送的字节数
    ret = send(clientfd,buffer,ret,0);


}

上面的代码已经把每个函数的参数的作用,还有 参数的意义都已经注释上了,
运行一下上面的代码,并使用我们的网络调试助手,发现我们的客户端已经可以收发数据了。
现在就遇到了一个问题,如果我们想一直接受和发送数据,我们需要做什么处理那?
可能大多数人都可以想到,我们添加 一个while循环的就可以一直接受数据了,因此我们改变一下我们的代码。
让它可以一直收发数据,知道我们的客户端退出为止。

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>



#define BUFFER_LENGTH 128


int main()
{
    //socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
    //这里使用0,表示让系统自动推导我们需要使用的传输协议。
    int listenfd= socket(AF_INET,SOCK_STREAM,0);
    //如果返回值为-1,说明我们创建SOCKET失败,直接返回。
    if (listenfd==-1)
    {
        return -1;
    }
    //我们需要绑定的信息
    struct sockaddr_in serveraddr;
    //使用IPV4
    serveraddr.sin_family=AF_INET;
    //我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //我们需要绑定的端口,这里我们绑定的端口为9999
    serveraddr.sin_port=htons(9999);
    //第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。    
    if (-1==bind(listenfd,(const  sockaddr*)&serveraddr,sizeof(serveraddr)))
    {
        return -2;        
    }
    //监听我们创建的套接字,请求的队列数量,这里我们填写为10个
    listen(listenfd,10);
    //定义客户端的socket
    struct sockaddr_in client;
    //客户端结构体的长度
    socklen_t len=sizeof(client);
    //等待接受连接
    //第一个参数服务器的套接字,第二个接收到的客户端的socket,第三个函数就是结构体的长度
    int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
    //接受的缓冲区大小
    unsigned char buffer[BUFFER_LENGTH]={0};
    while (1)
    {
       //收函数
        //第一个参数客户端的套接字,第二个参数,接受的缓冲区,第三个参数缓冲区的大小,第4个参数接收到的字节数
        int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
        if (ret<=0)
        {
            close(clientfd);
            break;
        }
        printf("buffer: %s , ret : %d\n",buffer,ret);
        //发函数
        //第一个参数客户端的套接字,第二个参数,发送的缓冲区,第三个参数发送的字节数,第4个参数实际发送的字节数
        ret = send(clientfd,buffer,ret,0);
    }
    
    
    return 0;

}

这样我们就达到了我们的要求,那么还有新的问题,就是我创建一个TCP server不能只使用一个。
下面就是如果我这个TCPserver想要连接多个客户端我应该怎么去做?
有两个可行的方案 ,供我们使用:

  1. 多线程、多进程的方式
  2. select,poll,epoll多路复用的方式。
    我们首先看一下第一种的方式
    多进程的方式,创建一个服务器,实现一个TCP 服务器可以连接多个TCP 客户端
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>



#define BUFFER_LENGTH 128


int main()
{

     unsigned char buffer[BUFFER_LENGTH]={0};
    //socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
    //这里使用0,表示让系统自动推导我们需要使用的传输协议。
    int listenfd= socket(AF_INET,SOCK_STREAM,0);
    //如果返回值为-1,说明我们创建SOCKET失败,直接返回。
    if (listenfd==-1)
    {
        return -1;
    }
    //我们需要绑定的信息
    struct sockaddr_in serveraddr;
    //使用IPV4
    serveraddr.sin_family=AF_INET;
    //我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //我们需要绑定的端口,这里我们绑定的端口为9999
    serveraddr.sin_port=htons(9999);
    //第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。    
    if (-1==bind(listenfd,(const  sockaddr*)&serveraddr,sizeof(serveraddr)))
    {
        return -2;        
    }
    //监听我们创建的套接字,请求的队列数量,这里我们填写为10个
    listen(listenfd,10);
    //定义客户端的socket
    ;
    while (1)
    {
        struct sockaddr_in client;
        //客户端结构体的长度
        socklen_t len=sizeof(client);
        //等待接受连接
        //第一个参数服务器的套接字,第二个接收到的客户端的socket,第三个函数就是结构体的长度
        int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
        if (clientfd<0)
        {
            close(listenfd);
            return -3;
        }
        
        pid_t id=fork();
        if (id<0)
        {
            perror("fork");
        }else if (id==0)
        {
           close(listenfd);
           pid_t idd= fork();
           if (idd<0)
           {
             perror("second fork");
             _exit(5);
           }
           else if (idd==0)
           {
                while (1)
            {
                //收函数
                //第一个参数客户端的套接字,第二个参数,接受的缓冲区,第三个参数缓冲区的大小,第4个参数接收到的字节数
                 int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
                if (ret<=0)
                {
                    close(clientfd);
                    break;
                }
                 printf("buffer: %s , ret : %d\n",buffer,ret);
        //发函数
        //第一个参数客户端的套接字,第二个参数,发送的缓冲区,第三个参数发送的字节数,第4个参数实际发送的字节数
                 ret = send(clientfd,buffer,ret,0);
        }
           }else{
             _exit(6);
           }
           

           
        }
        

#if 0
        //接受的缓冲区大小
        unsigned char buffer[BUFFER_LENGTH]={0};
        while (1)
        {
           //收函数
            //第一个参数客户端的套接字,第二个参数,接受的缓冲区,第三个参数缓冲区的大小,第4个参数接收到的字节数
            int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
            if (ret<=0)
            {
             close(clientfd);
                break;
            }
            printf("buffer: %s , ret : %d\n",buffer,ret);
        //发函数
        //第一个参数客户端的套接字,第二个参数,发送的缓冲区,第三个参数发送的字节数,第4个参数实际发送的字节数
            ret = send(clientfd,buffer,ret,0);
        }
#endif
    }
   
    
    
    return 0;

}

然后我们在看看通过第一种方式的多线程的方式来完成我们的TCP服务器

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>
#include<thread>


#define BUFFER_LENGTH 128

void routine(void *arg) {

	int clientfd = *(int *)arg;

	while (1) {
		
		unsigned char buffer[BUFFER_LENGTH] = {0};
		int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		if (ret == 0) {
			close(clientfd);
			break;
			
		}
		printf("buffer : %s, ret: %d\n", buffer, ret);

		ret = send(clientfd, buffer, ret, 0); // 

	}

}

int main()
{

     unsigned char buffer[BUFFER_LENGTH]={0};
    //socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
    //这里使用0,表示让系统自动推导我们需要使用的传输协议。
    int listenfd= socket(AF_INET,SOCK_STREAM,0);
    //如果返回值为-1,说明我们创建SOCKET失败,直接返回。
    if (listenfd==-1)
    {
        return -1;
    }
    //我们需要绑定的信息
    struct sockaddr_in serveraddr;
    //使用IPV4
    serveraddr.sin_family=AF_INET;
    //我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //我们需要绑定的端口,这里我们绑定的端口为9999
    serveraddr.sin_port=htons(9999);
    //第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。    
    if (-1==bind(listenfd,(const  sockaddr*)&serveraddr,sizeof(serveraddr)))
    {
        return -2;        
    }
    //监听我们创建的套接字,请求的队列数量,这里我们填写为10个
    listen(listenfd,10);
    //定义客户端的socket
    ;
    while (1)
    {
        struct sockaddr_in client;
        //客户端结构体的长度
        socklen_t len=sizeof(client);
        //等待接受连接
        //第一个参数服务器的套接字,第二个接收到的客户端的socket,第三个函数就是结构体的长度
        int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
        if (clientfd<0)
        {
            close(listenfd);
            return -3;
        }
        
        std::thread t{&routine,&clientfd};
        t.detach();

           
    }
        

#if 0
        //接受的缓冲区大小
        unsigned char buffer[BUFFER_LENGTH]={0};
        while (1)
        {
           //收函数
            //第一个参数客户端的套接字,第二个参数,接受的缓冲区,第三个参数缓冲区的大小,第4个参数接收到的字节数
            int ret = recv(clientfd,buffer,BUFFER_LENGTH,0);
            if (ret<=0)
            {
             close(clientfd);
                break;
            }
            printf("buffer: %s , ret : %d\n",buffer,ret);
        //发函数
        //第一个参数客户端的套接字,第二个参数,发送的缓冲区,第三个参数发送的字节数,第4个参数实际发送的字节数
            ret = send(clientfd,buffer,ret,0);
        }
#endif   
    
    return 0;

}

这样我们的就完成了我们TCP 单进程多线程服务器的创建,这里会有一个问题:如果我们就想要通过单线程完成我们的可以接受多个客户端的连接,我们应该怎么去实现那?
在这里,我大概讲一下我自己的理解,可能理解的不对,也是刚开始学,我是这样去理解,当我们使用单线程只能接受一个客户端的TCP服务器的时候,当我们连接第2个客户端的时候,ACCEPT是可以接收的,但是没有办法进行消息的收发,因此我们就有这样的一个思路去完成我们的需求,就是通过类似哈希表一样的结构,有一个新的连接连入我们的时候,我们就插入这个表,然后我们开始轮询遍历这个表中的连接,如果有读写的事件产生,我们就调用recv和send函数进行调用,完成我们的需求。

类似图片中的轮询,下面我们看一下使用select函数的代码是怎么样的。

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include <unistd.h>


#define BUFFER_LENGTH 128



int main()
{

     unsigned char buffer[BUFFER_LENGTH]={0};
     int ret=0;
    //socket有两个参数,第一个参数指定我们要使用IPV4,还是IPV6,第二个参数表明我们要使用套接字类型,这里我们使用的是流格式的套接字,第三个参数就是我们需要使用传输协议
    //这里使用0,表示让系统自动推导我们需要使用的传输协议。
    int listenfd= socket(AF_INET,SOCK_STREAM,0);
    //如果返回值为-1,说明我们创建SOCKET失败,直接返回。
    if (listenfd==-1)
    {
        return -1;
    }
    //我们需要绑定的信息
    struct sockaddr_in serveraddr;
    //使用IPV4
    serveraddr.sin_family=AF_INET;
    //我们需要绑定的IP地址,INADDR_ANY 就是0.0.0.0 ,就是所有网卡的所有IP段都可以连接到我们的创建的TCP服务器上。
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //我们需要绑定的端口,这里我们绑定的端口为9999
    serveraddr.sin_port=htons(9999);
    //第一个参数我们创建的套接字,第二个是我们填写的绑定信息,最后是我们的绑定信息结构体的大小。    
    if (-1==bind(listenfd,(const  sockaddr*)&serveraddr,sizeof(serveraddr)))
    {
        return -2;        
    }
    //监听我们创建的套接字,请求的队列数量,这里我们填写为10个
    listen(listenfd,10);
    //定义客户端的socket
   
    //定义可读序列和可写序列
    fd_set rfds,wfds,rset,wset;
    //清空序列
    FD_ZERO(&rfds);
    //设置读的序列
    FD_SET(listenfd,&rfds);
    //清空可写的序列
    FD_ZERO(&wfds);

    int maxfd=listenfd;
    
    while (1)
    {
        //开始进行序列的赋值,
        rset=rfds;
        wset=wfds;

        //select开始多路服用
        //第一个参数是所有文件描述符的范围,第二个参数监控读的文件描述符的序列,第三个参数监控写的文件描述符的序列, 第4个参数监控异常的序列,第5个参数等待的时间,0是指无限等待
        int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
        //判断listenfd服务器socket是否被设置,设置代表有效。
        if (FD_ISSET(listenfd,&rset))
        {
            printf("listen --> \n");
            struct sockaddr_in client;
			socklen_t len = sizeof(client);
            	//开始接受客户端得连接
			int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
            //添加可读列表中
			FD_SET(clientfd, &rfds);
            //如果客户端的文件描述符的序号大于最大的文件描述符的编号
			if (clientfd > maxfd) maxfd = clientfd;
        }
        int i = 0;
        //开始轮询监听的已经连接的客户端的socket
		for (i = listenfd+1; i <= maxfd;i ++) {
            //如果有可读的事件
			if (FD_ISSET(i, &rset)) { //
                //开始接受信息
				ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret <= 0) {
					close(i);
					FD_CLR(i, &rfds);
					
				} else if (ret > 0) {
                    //打印接受到信息
					printf("buffer : %s, ret: %d\n", buffer, ret);
                    //设置可写的事件
					FD_SET(i, &wfds);
				}
				//如果有可写的事件
			} else if (FD_ISSET(i, &wset)) {
				//开发发送消息
				ret = send(i, buffer, ret, 0); // 
				//清空可写的事件
				FD_CLR(i, &wfds); 
                //设置可读事件,因为并没有断开连接
				FD_SET(i, &rfds);
				

			}

		}
		
    }
        
    return 0;

}

这样就完成了我们的select选择模型的代码。今天我们就介绍到这里。

推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:
服务器
音视频
dpdk
Linux内核

posted @ 2022-10-08 21:04  飘雨的河  阅读(37)  评论(0编辑  收藏  举报