网络IO与IO多路复用(select/poll/epoll)(一)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

对网络通信的学习做一下记录总结,以下均在linux的环境下进行操作。(后面出现了一点错误,开发环境改为windows环境下,启用虚拟机进行操作)


提示:以下是本篇文章正文内容,下面案例可供参考

一、网卡

网卡作为TCP/IP层的接口,可以在物理层传输信号,在网络层传输数据包。无论位于哪个层,它都充当计算机或服务器和数据网络之间的中间媒介。当用户发送一个web页面请求时,网卡从用户设备中获取数据,并将其发送到网络服务器,然后接收所需的数据展示给用户。
我们可以通过ifconfig命令对电脑的相关网络配置进行查看。请添加图片描述我这里由于连接的是手机热点所以显示的是wlo1。一般电脑连接有线网线显示可能是eth0,或者是eno、ens或enp。下面将对其做简要的介绍。

ens、eno、enp网口的区别

扩展知识内容:

en标识ethernet
o:主板板载网卡,集成是的设备索引号
p:独立网卡,PCI网卡
s:热插拔网卡,USB之类的扩展槽索引号
nnn(数字):MAC地址+主板信息计算得出唯一序列

eno1:代表由主板bios内置的网卡

ens1:代表有主板bios内置的PCI-E网卡

enp2s0: PCI-E独立网卡

eth0:如果以上都不使用,则回到默认的网卡名

大概可以这样理解:ens37f1np1、ens37f1

ens表示热插拔网卡,37表示槽位,“f1”:可能是指具体的功能或标识,如网口类型或配置。“np1”:可能表示某种特定的网络协议或功能的缩写。(普通以太网网卡插上:ens37f1,mellanox rdma 网卡插上:ens37f1np1 ,(CI环境)intel rdma网卡插上:ens1f1)
(摘自:https://blog.csdn.net/bandaoyu/article/details/116308950)

二、网络IO

1.代码

multi-io.c代码如下:

#include <sys/socket.h>  // 包含 socket 系列函数的头文件
#include <errno.h>       // 包含错误处理的头文件
#include <netinet/in.h>  // 包含 IPv4 地址结构的头文件
#include <stdio.h>       
#include <string.h>    

// 创建 TCP Socket
int main() {

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建套接字描述符
    struct sockaddr_in serveraddr;                // 定义服务器地址结构
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));  // 清零地址结构

    serveraddr.sin_family = AF_INET;              // 指定地址家族为 IPv4
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);        // 指定任意本地地址
    serveraddr.sin_port = htons(2048);            // 指定端口号为 2048

    // 绑定套接字到指定地址
    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
        perror("bind");  // 打印错误信息
        return -1;       // 返回错误码
    }

    listen(sockfd, 10);  // 监听套接字,允许最大连接数为 10

    getchar();  // 等待用户输入

    return 0;  
}

如果我们程序想进行网络通信首先得创建一个socket,它提供了一个抽象层,使程序能够通过网络进行通信(相当于我们开启了程序通过网络通信的出入口)。随后我们需要提供网络通信的相关地址信息,即代码中的sockaddr,然后通过bind函数将地址绑定到我们的socket上。此时我们就已经对某个地址中的某个端口进行了连接。
我们将其生成为可执行文件

gcc -o multi-io multi-io.c

运行后可以观察到程序正在等待我们用户的输入。
请添加图片描述
那么我们怎么知道端口是否已经连接呢?可以通过netstat -anop | grep 2048查看该端口的连接状态。
请添加图片描述(如果在非root权限下运行会出现上述提示,可在命令前加上sudo或切换为root权限下运行即可)
可看到该端口已被占用处于监听状态。

补充

下面对代码中的一些参数含义进行补充。

1.socket

请添加图片描述
这是官方对socket函数的描述,它的返回值为一个整数,如果其值为-1,则代表发生了错误可使用perror将错误打印出来,它一共包含三个参数。
1.__domain:指定了通信的地址类型,常见的有:

  • AF_INET:IPv4地址类型
  • AF_INET6:IPv6地址类型
  • AF_UNIX:Unix域(本地)工具

2.__type:指定通信方式,常见的有:

  • SOCK_STREAM:通过数据流的方式进行数据传输,提供可靠的、面向连接的通信,使用TCP协议
  • SOCK_DGRAM:通过数据报方式进行数据传输,提供无连接、不可靠的通信,使用UDP协议。
  • SOCK_RAW:原始工具,可以直接访问简单的网络协议

3.__protocol:表示设备使用的协议。如果参数为0,则系统会根据地址和设备类型自动选择合适的协议。对于 IPv4 和 IPv6,常见的协议有:

  • IPPROTO_TCP:表示TCP协议
  • IPPROTO_UDP:表示UDP协议

2.sockaddr_in
这是用于存储 IPv4 地址信息的结构体,IPv6存储的结构体为sockaddr_in6,具体定义如下:

请添加图片描述
我们在结构体中没有看到sin_family成员,我猜测其时从父类继承过来的,此处我们不是深究。
请添加图片描述一般我只需要定义这三个成员变量就可以正常使用,而sin_zero用于将结构体大小补齐至16字节,通常不需要使用。特别注意的是INADDR_ANY表示任意本地地址即0.0.0.0,他表示允许连接到任意本地IP地址。

3.bind
将设备(此处指socket)绑定到本地地址(IP 地址和端口号)的函数,具体定义参数如下:
请添加图片描述
三个参数依次表示(绑定设备,绑定地址,地址长度)。
(struct sockaddr*)&serveraddr可能有些朋友会看到比较疑惑,其实是为了取到结构体的地址后进行指针的强制转换。

2.使用tcp/udp net assistant工具

使用该工具可以对我们所创建的程序进行更为直观的测试。
下载地址:https://github.com/busyluo/NetAssistant
使用步骤:
git clone https://github.com/busyluo/NetAssistant.git
cd NetAssistant
qmake
make
./NetAssistant

成功允许后的界面:
请添加图片描述
第一次尝试在linux系统上进行连接,由于我的电脑上仅有一张网卡导致连接失败,建议在windows环境下在虚拟机上进行操作。

以下是在windows环境下在虚拟机上进行操作。
windows下的tcp/udp net assistant工具
下载地址:http://www.cmsoft.cn/download/cmsoft/netassist.zip
解压后直接运行。(这个软件可能会提示报错有病毒的情况,需要把实时防火墙暂时关闭才能下载使用)
在这里插入图片描述
在虚拟机上运行ifconfig
在这里插入图片描述
可以观察到ip地址为192.168.237.134
将assistant的参数进行设置
协议类型:TCP_Client
远程主机地址:192.168.237.134
远程主机端口:2048

点击连接
可以在数据日志上看到连接的信息(上图的assistant是连接后所截图片)
同时我们可以使用netstat -anop | grep 2048查看端口的使用情况。
在这里插入图片描述
可以看到已经连接成功。此出我们代码所创建的就是一个服务器端,而用assistant程序运行的是一个客户端,这样我们就实现了客户端和服务器端的TCP连接。
在第五列显示的LISTEN、ESTABLISHED为TCP的连接状态,TCP共有11种连接状态,具体的可以看一下这个博主写的文章:
https://blog.csdn.net/weixin_44560620/article/details/116449785

3.实现程序与assistant之间的通信

multi-io.c代码如下:

#include <sys/socket.h>  // 包含 socket 系列函数的头文件
#include <errno.h>       // 包含错误处理的头文件
#include <netinet/in.h>  // 包含 IPv4 地址结构的头文件
#include <stdio.h>       
#include <string.h>    
#include <unistd.h> 	//close
#include <fcntl.h>		//open(可不加)

// 创建 TCP Socket
int main() {
    // 创建套接字描述符
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 定义服务器地址结构
    struct sockaddr_in serveraddr;
    
    // 清零地址结构
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));

    // 指定地址家族为 IPv4
    serveraddr.sin_family = AF_INET;
    
    // 指定任意本地地址
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    // 指定端口号为 2048
    serveraddr.sin_port = htons(2048);

    // 绑定套接字到指定地址
    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
        perror("bind");  // 打印错误信息
        return -1;       // 返回错误码
    }

    // 监听套接字,允许最大连接数为 10
    listen(sockfd, 10);

    // 接受客户端连接
    struct sockaddr_in clientaddr;          
    socklen_t len = sizeof(clientaddr);       
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("accept\n");

    // 接收数据
    char buffer[128] = {0};
    int count = recv(clientfd, buffer, 128, 0);
    
    // 发送数据
    send(clientfd, buffer, count, 0);

    // 打印相关信息
    printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);

    getchar();  // 等待用户输入

    // 关闭连接
    close(clientfd); 

    return 0;  
}

此处代码结构与上文类似,且在每段代码后面加上了注释,此处就不赘述了。
我们重新编译运行代码后用assistant进行连接,随后我们在assistant上对数据进行发送。
在这里插入图片描述
我们可以在终端观察到我们所发送的数据信息。同时我们又将数据信息重新发送回了assistant上,在assistant上我们可以看到接收到的数据信息(此处向assistant发送信息是为了作为验证,证明我们成功接收到了从assistant发送的信息,其实通过打印同样可以验证,但是此处我又多加了一层)。
在这里插入图片描述
但是同时我们也发现当我们再次点击发送时,数据不会在被接收,这是由于我们的代码目前只接受了一次循环接收,那如何实现多次信息的接收呢,其实最简单的办法是我们给其加上一层while循环。
补充
sockfd和clientfd文件描述符的值为什么分别为3和4呢?
这是因为操作系统可能已分别将文件描述符 0、1 和 2 分配给标准输入、标准输出和标准错误。当您使用 打开套接字时socket(AF_INET, SOCK_STREAM, 0),操作系统会分配下一个可用文件描述符,即 3。然后,当您使用 接受连接时,accept(sockfd, (struct sockaddr*)&clientaddr, &len)操作系统会为客户端套接字分配下一个可用文件描述符,即 4。
我们可以通过ls /dev/std* -l命令进行验证。
请添加图片描述

4.实现程序与assistant之间的循环多次通信

multi-io.c代码如下:

#include <sys/socket.h>  // 包含 socket 系列函数的头文件
#include <errno.h>       // 包含错误处理的头文件
#include <netinet/in.h>  // 包含 IPv4 地址结构的头文件
#include <stdio.h>       
#include <string.h>    
#include <unistd.h> 	//close
#include <fcntl.h>		//open(可不加)

// 创建 TCP Socket
int main() {
    // 创建套接字描述符
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 定义服务器地址结构
    struct sockaddr_in serveraddr;
    
    // 清零地址结构
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));

    // 指定地址家族为 IPv4
    serveraddr.sin_family = AF_INET;
    
    // 指定任意本地地址
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    // 指定端口号为 2048
    serveraddr.sin_port = htons(2048);

    // 绑定套接字到指定地址
    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
        perror("bind");  // 打印错误信息
        return -1;       // 返回错误码
    }

    // 监听套接字,允许最大连接数为 10
    listen(sockfd, 10);

    // 接受客户端连接
    struct sockaddr_in clientaddr;          
    socklen_t len = sizeof(clientaddr);       
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("accept\n");

#if 0
    // 单次接收和发送数据
    char buffer[128] = {0};
    int count = recv(clientfd, buffer, 128, 0);
    send(clientfd, buffer, count, 0);
    printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);

#else
    // 循环接收和发送数据
    while (1) {
        char buffer[128] = {0};
        int count = recv(clientfd, buffer, 128, 0);
        send(clientfd, buffer, count, 0);
        printf("sockfd: %d, clientfd: %d, count: %d, buffer: %s\n", sockfd, clientfd, count, buffer);
    }
#endif

    getchar();  // 等待用户输入
    close(clientfd);  // 关闭连接

    return 0;  
}

加上循环当我们再次编译运行之后,重新将数据进行发送,此时已经可以实现数据的循环接收和通信。
在这里插入图片描述
在这里插入图片描述
问题
但是此时当我们点击断开后,我们也会发现一个问题,我们的程序陷入了死循环。这是由于我们循环是个死循环,没有跳出操作,在其中也没有调用close()函数。
在这里插入图片描述
此外我们可以观察到当陷入死循环时,我们程序接收到的字符长度为0。所以我们可以对我们的程序做以下改变

while(1){
        char buffer[128]={0};
        int count=recv(clientfd,buffer,128,0);
        if(count==0)
        {
            break;
        }
        send(clientfd,buffer,count,0);
        printf("sockfd: %d,clientfd: %d,count: %d,buffer: %s\n",sockfd,clientfd,count,buffer);
        
    }

在循环中加入一个break,进行终止。其实根据原本我们的代码语义此处加入close()会更好,但是基于我们目前的代码结构加入break同样可以。

4.实现程序(服务端)与多个assistant(客户端)之间的通信

我们可以先试一下在此时代码的情况下,开启多个assistant进行通信会发生什么,我们可以发现我们可以进行连接的,但是我们的程序(服务端)只可以和一个assistant(客户端)进行通信。
我想到比较简单的方法就是利用线程开启多个accept,对我们客户端的程序进行接收。当然肯定有其他更好的办法,我们学习初期先以此种方法为例进行学习。
multi-io.c代码如下:

#include <sys/socket.h>  // 包含 socket 系列函数的头文件
#include <errno.h>       // 包含错误处理的头文件
#include <netinet/in.h>  // 包含 IPv4 地址结构的头文件
#include <stdio.h>       
#include <string.h>    
#include <unistd.h> 	//close
#include <fcntl.h>		//open(可不加)
#include <pthread.h>	//创建线程的头文件

//创建客户端线程
void *client_thread(void *arg)
{
    // 从参数中获取客户端套接字描述符
    int clientfd = *(int *)arg;

    // 循环处理客户端数据
    while (1)
    {
        char buffer[128] = {0};
        int count = recv(clientfd, buffer, 128, 0);

        // 如果接收到的数据长度为0,表示客户端关闭连接,退出循环
        if (count == 0)
        {
            break;
        }

        // 发送接收到的数据回客户端
        send(clientfd, buffer, count, 0);
        printf("clientfd: %d, count: %d, buffer: %s\n", clientfd, count, buffer);
    }

    // 关闭客户端连接
    close(clientfd);

    return NULL;
}

// 创建 TCP Socket
int main()
{
    // 创建套接字描述符
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 定义服务器地址结构
    struct sockaddr_in serveraddr;

    // 清零地址结构
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));

    // 指定地址家族为 IPv4
    serveraddr.sin_family = AF_INET;

    // 指定任意本地地址
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 指定端口号为 2048
    serveraddr.sin_port = htons(2048);

    // 绑定套接字到指定地址
    if (-1 == bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr)))
    {
        perror("bind");  // 打印错误信息
        return -1;       // 返回错误码
    }

    // 监听套接字,允许最大连接数为 10
    listen(sockfd, 10);

    // 循环等待客户端连接
    while (1)
    {
        // 接受客户端连接
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);

        // 创建线程处理客户端数据
        pthread_t thid;
        pthread_create(&thid, NULL, client_thread, &clientfd);
    }

    getchar();  // 等待用户输入
    // close(clientfd);

    return 0;
}

我们编译运行,就可以实现我们多个客户端的通信了。我在此处设置了两个客户端分别给服务端发送了数据。
在这里插入图片描述
在这里插入图片描述
消息数据可以成功接收和发送。


总结

本文仅简单介绍了网络IO的基础知识,通过代码和assistant工具实现简单的网络通信,下篇我们再来分享IO多路复用。不得当之处请多指教,共同勉励。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值