网络编程IO与select模型

本文深入介绍了网络IO的基本概念,特别是socket编程,并通过示例展示了服务器如何处理多个客户端连接。文章还探讨了阻塞IO的问题,并引入了select模型来解决并发连接的问题,解释了select函数的工作原理及其在处理多路IO时的角色。最后,通过代码示例展示了select在实际服务器模型中的应用。
摘要由CSDN通过智能技术生成

前言

        本文旨在介绍网络io的基本原理,与select模式是如何做到增加并发量的。

  本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。
 

socket编程


socket介绍


  传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信( 本质上借助内核-内核提供了socket伪文件的机制实现通信----实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库。

  使用socket会建立一个socket pair,如下图, 一个文件描述符操作两个缓冲区。

        几乎所有的IO 接口 ( 包括socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求

        

服务器代码原型【一请求一响应模式】

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

#define MAXLNE  4096

int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
    
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    printf("========waiting for client's request========\n");
    while (1) {
        
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
            
            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
    
    close(listenfd);
    return 0;
}

问题

        可以连接多个客户端,但是只有第一个连接的客户端可以传输数据

        为什么后续的客户端可以连接?

        因为listenfd在应用层启动的

-> 修改

        使用while循环轮询listen代码

while(1){
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
            
            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
}

        -> 多客户端可连接,连接完成后只能发送一条数据

【fd阻塞】

        -> 阻塞在accept之后,无法完成,无法接受新的数据

主要API函数介绍

socket

int socket(int domain, int type, int protocol);

函数描述: 创建socket

参数说明:

domain: 协议版本

- - AF_INET IPV4
- - AF_INET6 IPV6
- - AF_UNIX AF_LOCAL本地套接字使用

type:协议类型

- - SOCK_STREAM 流式, 默认使用的协议是TCP协议
- - SOCK_DGRAM  报式, 默认使用的是UDP协议

protocal:

- - 一般填0, 表示使用对应类型的默认协议.

返回值:

- - 成功: 返回一个大于0的文件描述符
- - 失败: 返回-1, 并设置errno

  当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列(监听文件描述符才有,listenFd)
 

bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数描述: 将socket文件描述符和IP,PORT绑定

参数说明:

  • socket: 调用socket函数返回的文件描述符
  • addr: 本地服务器的IP地址和PORT
  • addrlen: addr变量的占用的内存大小

返回值:

  • 成功: 返回0
  • 失败: 返回-1, 并设置errno

listen

int listen(int sockfd, int backlog);

函数描述: 将套接字由主动态变为被动态

参数说明:

  • sockfd: 调用socket函数返回的文件描述符
  • backlog: 在linux系统中,这里代表全连接队列(已连接队列)的数量。在unix系统种,这里代表全连接队列(已连接队列)+ 半连接队列(请求连接队列)的总数

返回值:

  • 成功: 返回0
  • 失败: 返回-1, 并设置errno
     

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);    

函数说明:获得一个连接, 若当前没有连接则会阻塞等待.

函数参数:

  • sockfd: 调用socket函数返回的文件描述符
  • addr: 传出参数, 保存客户端的地址信息
  • addrlen: 传入传出参数, addr变量所占内存空间大小

返回值:

  • 成功: 返回一个新的文件描述符,用于和客户端通信
  • 失败: 返回-1, 并设置errno值.

  accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
  从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
 

connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数说明: 连接服务器

函数参数:

  • sockfd: 调用socket函数返回的文件描述符
  • addr: 服务端的地址信息
  • addrlen: addr变量的内存大小

返回值:

  • 成功: 返回0
  • 失败: 返回-1, 并设置errno值

服务器模型-select

select介绍

  多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

数据类型fd_set::文件描述符集合——本质是位图

函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生

参数说明:

  • nfds: 最大的文件描述符+1
  • readfds: 读集合, 是一个传入传出参数

        传入: 指的是告诉内核哪些文件描述符需要监控
        传出: 指的是内核告诉应用程序哪些文件描述符发生了变化

  • writefds: 写文件描述符集合(传入传出参数,同上)
  • execptfds: 异常文件描述符集合(传入传出参数,同上)
  • timeout:
		NULL--表示永久阻塞, 直到有事件发生
		0   --表示不阻塞, 立刻返回, 不管是否有监控的事件发生
		>0  --到指定事件或者有事件发生了就返回
  • 返回值: 成功返回发生变化的文件描述符的个数。失败返回-1, 并设置errno值。

select-api

将fd从set集合中清除

void FD_CLR(int fd, fd_set *set);

功能描述: 判断fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否则返回0

int FD_ISSET(int fd, fd_set *set);

将fd设置到set集合中

void FD_SET(int fd, fd_set *set);

 初始化set集合

void FD_ZERO(fd_set *set);

 用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生

int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select优缺点


select优点:

  1. select支持跨平台

select缺点:

  1. 代码编写困难
  2. 会涉及到用户区到内核区的来回拷贝
  3. 当客户端多个连接, 但少数活跃的情况, select效率较低(例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下)
  4. 最大支持1024个客户端连接(select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的, 而是由FD_SETSIZE=1024限制的)

        FD_SETSIZE=1024 fd_set使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做。

select代码运行流程图

select代码实现

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

#include <pthread.h>

#define MAXLNE  4096


void *client_routine(void *arg){
    //子线程只做 接收 和 发送
    int connfd = *(int *)arg;
    char buff[MAXLNE];

    while (1) {
        
        int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
            
            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
            break;
        }

    }
    return NULL;
}



int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
    
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    #if 0

    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    
    printf("========waiting for client's request========\n");
    while (1) {
        
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
            
            send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }
        
        //close(connfd);
    }
    # elif 0
    while (1) {

		struct sockaddr_in client;
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }

		pthread_t threadid;
		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);

    }

    # else
    
    fd_set rfds,rset;

    FD_ZERO(&rfds);
    FD_SET(listenfd,&rfds);

    int max_fd = listenfd;
    while(1){
        rset = rfds;
        int nready = select(max_fd + 1, &rset , NULL , NULL , NULL);
        if(FD_ISSET(listenfd,&rset)){

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
                printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
                return 0;
            }
        FD_SET(connfd,&rfds);
        if(connfd > max_fd) max_fd = connfd;
        if(--nready == 0) continue;

        }

        int i = 0;
        for(i = listenfd + 1;i <= max_fd;i ++){
            if(FD_ISSET(i,&rset)){
                n = recv(i, buff, MAXLNE, 0);
                if (n > 0) {
                    buff[n] = '\0';
                    printf("recv msg from client: %s\n", buff);
                    
                    send(i, buff, n, 0);
                } else if (n == 0) {
                    FD_CLR(i,&rfds);
                    close(i);
                }

            }

        }
    }


    # endif
    close(listenfd);
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值