提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
网络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多路复用。不得当之处请多指教,共同勉励。