Netcat软件的基本使用
Netcat(简写nc)是一个强大的网络命令工具,能够在linux中执行与TCP、UDP相关的操作,例如端口扫描,端口重定向、端口监听甚至远程连接。
在这里,我们使用 nc 来模拟一台接收message的服务器,和一台发送message的客户端。
1、安装 nc 软件
sudo yum install -y nc
2、使用 nc 创建一台监听9999端口的服务器
nc -l -p 9999 # -l表示listening,监听
启动成功后 nc 进行阻塞
3、新建一个bash,使用 nc 创建一个发送message的客户端
nc localhost 9999
在控制台上输入要发送的信息,查看服务端是否接收到
4、查看上面的nc进程中的文件描述符
ps -ef | grep nc # 查看nc的进程号,这里假设是2603
ls /proc/2603/fd # 查看2603进程下的文件描述符
[图片上传中...(image-f69c24-1615989342820-15)]
可以看到这个进程下有一个socket,这就是nc的客户端和服务端之间创建的一个socket
经过这一系列的操作,相信我们对Netcat软件有了基本的了解,下面来介绍BIO
strace追踪系统调用
strace软件说明: 它是一个可以追踪系统调用和信号的软件,通过它我们来了解BIO
环境说明: 这里演示的都是基于老版本的linux,因为新版本的linux都不用BIO了,演示不出来
1、使用strace来追踪系统调用
sudo yum install -y strace # 安装strace软件
mkdir ~/strace # 新建一个目录,存放追踪的信息
cd ~/strace # 进入到这个目录
strace -ff -o out nc -l -p 8080 # 使用strace追踪后边的命令进行的系统调用
# -ff 表示追踪后面命令创建的进程及子进程的所有系统调用,
# 并根据进程id号分开输出到文件
# -o 表示追踪到的信息输出到指定名称的文件,这里是out
2、查看服务端创建的系统调用
在上一步进入的目录下,出现了一个 out.pid 文件,里的内容都是 nc -l -p 9999 这个命令执行后的系统调用过程,使用vim命令来查看
vim out.92459 # nc进程id为92459
这里accept()方法进行了阻塞,它要等待其他socket对它进行连接
3、客户端连接,查看系统调用
退出vim,使用tail来进行查看
tail -f out.92459
-f 参数:当文件有追加的内容,可以实时地打印在控制台,这样就能很方便来查看客户端连接后进行的系统调用
nc localhost 8080
查看系统调用
这里客户端连接后,accept() 方法获取到客户端连接并返回文件描述符4,这个4就是服务端新创建的socket,用于和这个客户端进行通信
之后使用多路复用器poll来监听服务端上文件描述符4和0,0是标准输入文件描述符,哪个有事件发生就读取哪个文件描述符,如果都没有事件发生就进行堵塞
4、客户端发送message,查看系统调用
客户端向服务端发送数据,服务端就能从socket中监听到有事件发生,就能进行相应的处理,处理完继续堵塞,等待下一个事件发生
5、服务端发送数据到客户端,查看系统调用
服务端发数据,肯定从键盘输入,也就是标准输入0,从0中读取到数据发送给socket 4
BIO(阻塞式IO)
在我们第三节中,我们使用 strace 工具查看了 nc 软件使用过程中的系统调用,其实上一节中体现的就是BIO,我们把上面的一系列系统调用总结一下,根据直观的理解BIO
1、单线程模式
1.1、过程演示
1、服务端启动
启动服务端,等待socket连接,accept()方法阻塞
2、客户端连接,未发送数据
连接客户端,accept() 方法执行,未收到client1发送的数据,read()方法阻塞
3、另一个客户端连接
由于read()方法阻塞,无法执行到accept()方法,所以这样cpu一次只能处理一个socket
1.2、存在的问题
上面的模型存在很大的问题,如果客户端与服务端建立了连接,客户端迟迟不发数据,进程就会一直堵塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好
1.3、如何解决
其实要解决这个问题很简单,利用多线程就可以,只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个线程上,不堵塞主线程,就能操作多个socket了,有哪个线程中的socket有数据,就读哪个socket
2、多线程模式
1.1 过程演示
- 程序服务端只负责监听是否有客户端连接,使用 accept() 阻塞
- 客户端1连接服务端,就开辟一个线程(thread1)来执行 read() 方法,程序服务端继续监听
- 客户端2连接服务端,也开辟一个线程,执行read()方法
- 任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理
1.2、存在的问题
上面这个多线程模型,看似已经十分的完美,其实也有很大的问题。每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。在操作系统中,用户态不能直接开辟线程,需要调用cpu的80软中断,让内核来创建的一个线程,这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。
1.3、如何解决
第一个办法:使用线程池,这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行
第二个办法:因为read()方法堵塞了,所有要开辟多个线程,如果什么方法能使read()方法不堵塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO)
NIO(非阻塞式IO)
1、过程演示
1、服务端刚创建,没有客户端连接
在NIO中,accept()方法也是非阻塞的,它在一个while死循环中
2、当有一个客户端进行连接时
3、当有第二个客户端进行连接时
2、总结
在NIO模式中,一切都是非阻塞的:
- accept()方法是非阻塞的,如果没有客户端连接,就返回error
- read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read()方法读数据的时间
在NIO模式中,只有一个线程:
- 当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据
- 这样一个线程就能处理多个客户端的连接和读取了
3、存在的问题
NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,看似已经 perfect,但是还存在问题。
这个模型在客户端少的时候十分好用,但是客户端如果很多,比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会变量一万个socket,就会做很多无用功。而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大
因为这些问题的存在,IO多路复用应运而生
IO Multiplexing(IO多路复用)
IO多路复用有三种实现方式,select、poll、epoll,现在让我们来看看这三种实现的真面目吧
1、select
1.1 优点
select 其实就是把NIO中用户态要遍历的 fd 数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了
从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率
1.2 存在的问题
1、bitmap最大1024位,一个进程最多只能处理1024个客户端2、&rset不可重用,每次socket有数据就相应的位会被置位3、文件描述符数组拷贝到了内核态,仍然有开销4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
2、poll
2.1 代码例子
在poll中,文件描述符有一份独立的数据结构pollfd,传入poll中的是pollfd的数组,其他的实现逻辑和select一样
2.2 优点
1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回0,实现了pollfd数组的重用
2.3 缺点
poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题
1、pollfds数组拷贝到了内核态,仍然有开销2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历
3、epoll
3.1 代码例子
3.2 事件通知机制
1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中2、网卡向cpu发起中断,让cpu先处理网卡的事3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中
3.3 详细过程
首先epoll_create创建epoll实例,它会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄,其实就是在内核开辟一块内存空间,所有与服务器连接的socket都会放到这块空间中,这些socket以红黑树的形式存在,同时还会有一块空间存放就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。
3.4 水平触发和边沿触发
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
3.5 优点
epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll
1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket
Linux、C/C++技术交流群 整理了一些个人觉得比较好Linux服务器架构师学习书籍、大厂面试题、和热门技术教学视频资料(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享有需要的可以自行添加哦!
以上不足的地方欢迎指出讨论,觉得不错的朋友,希望能得到您的点赞支持
有疑问加站长微信联系(非本文作者)