背景
这篇博客用来记录网络io到多路复用视频课程的实验。
创建tcp链接后,生成了一个socket并返回文件描述符重定向到8.
# 8代表文件描述符 < 输入 > 输出
$ exec 8<> /dev/tcp/www.baidu.com/80
# $$表示当前解释器的进程
# 进入到文件描述目录下
$ cd /proc/$$/fd
$ pwd
/proc/20222/fd
# 0 标准输入流 1 标准输出流 2 标准错误流
# 8 指向了一个socket
# 出现socket后,tcp协议已经建立完成
$ ll
total 0
lrwx------. 1 root root 64 Nov 23 11:18 0 -> /dev/pts/1
lrwx------. 1 root root 64 Nov 23 11:18 1 -> /dev/pts/1
lrwx------. 1 root root 64 Nov 23 11:18 2 -> /dev/pts/1
lrwx------. 1 root root 64 Nov 23 15:56 255 -> /dev/pts/1
lrwx------. 1 root root 64 Nov 23 15:56 8 -> socket:[114771]
# 获取百度主页,走应用层协议。
# 1. 使用GET方法获取百度页面
# 2. -e参数为了转换\n换行符
# 3. 1表示标准输出重定向到文件描述符8,当重定向地址不是文件时需要使用&
$ echo -e 'GET / HTTP/1.0\n' 1>& 8
# 获取返回内容
# cat的标准输入0来自于重定向的8
$ cat 0<& 8
HTTP/1.0 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Content-Length: 14615
...
tcp:
面向连接的,可靠的传输协议。3次握手,4次挥手
使用tcpdump来抓包,一次tcp链接所发包的详细过程。
# 1. 创建tcpdump来监听网卡是eth0,端口号是80·
$ tcpdump -nn -i eth0 port 80
# 2. curl www.baidu.com 向百度发送数据请求.
$ curl www.baidu.com
# 3. 收到返回消息
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
# 1. 本机的随机端口号,发送给baidu服务器固定的默认80端口,一个请求握手的包,Sync
09:32:11.899274 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [S], seq 2068494884, win 29200, options [mss 1460,sackOK,TS val 3608087326 ecr 0,nop,wscale 7], length 0
# 2. tcp环节,baidu返回一个握手确认的包, Sync + ack
09:32:11.904843 IP 14.215.177.38.80 > 192.168.0.182.52166: Flags [S.], seq 1922772699, ack 2068494885, win 8192, options [mss 1452,sackOK,n op,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 5], length 0
# 3. tcp环节,本机回复baidu一个握手确认的包,ack
09:32:11.904886 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [.], ack 1, win 229, length 0
# 此处,握手完成后,tcp链接建立完成,双方开辟物理资源。
# 4. 本地给baidu发送一个数据包,HTTP包的请求头,长度为77, [P]触发内核中断,交给上层服务处理
09:32:11.904951 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [P.], seq 1:78, ack 1, win 229, length 77: HTTP: GET / HTTP/1.1
# 5. baidu回复本机一个确认, ack, 长度为0
09:32:11.910543 IP 14.215.177.38.80 > 192.168.0.182.52166: Flags [.], ack 78, win 908, length 0
# 6. baidu发送给客户端一个数据包, 200 OK, 长度为2781,[P]触发内核中断,交给上层服务处理
09:32:11.911429 IP 14.215.177.38.80 > 192.168.0.182.52166: Flags [P.], seq 1:2782, ack 78, win 908, length 2781: HTTP: HTTP/1.1 200 OK
# 7. 客户端给baidu发送一个确认,ack, 长度为0
09:32:11.911440 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [.], ack 2782, win 272, length 0
# 最后,客户端给服务发送一个断开的链接。4次分手过程
# 8. 本地给baidu发送一个断开链接请求
09:32:11.911552 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [F.], seq 78, ack 2782, win 272, length 0
# 9. baidu给本机发送一个确定, ack
09:32:11.917116 IP 14.215.177.38.80 > 192.168.0.182.52166: Flags [.], ack 79, win 908, length 0
# 10. baidu给本机发送一个断开链接请求
09:32:11.917118 IP 14.215.177.38.80 > 192.168.0.182.52166: Flags [F.], seq 2782, ack 79, win 908, length 0
# 11. 本机给baidu发送一个确认,ack
09:32:11.917144 IP 192.168.0.182.52166 > 14.215.177.38.80: Flags [.], ack 2783, win 272, length 0
nc
# 1. 利用nc创建一个服务端
$ nc -l 8080
# 2. 服务端发送请求
$ nc localhost 8080
# 实验中,先启动一个服务端,不开启客户端
$ nc -l 8080
# 1. 查看nc的进程下的描述符(这里有两个文件描述符???)
$ ps -ef | grep nc
$ ll /proc/XXXX/fd
lrwx------ 1 root root 64 Nov 24 10:03 0 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 1 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 2 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 3 -> socket:[751943] #
# 2. 目前,并没有创建客户端的链接,但是出现了两个socket
# 此处的一个socket是用来监听(并且仅有服务端才有监听的概念)
# 3. 新创建了一个客户端,4号描述符代表链接成功了(前两个文件描述符消失,创建出一个新的文件描述符)
$ ll /proc/XXXX/fd
lrwx------ 1 root root 64 Nov 24 10:03 0 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 1 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 2 -> /dev/pts/2
lrwx------ 1 root root 64 Nov 24 10:03 3 -> socket:[751943] #
lrwx------ 1 root root 64 Nov 24 10:03 4 -> socket:[751944]
strace
# -ff 用来抓取将要执行的进程线程的对内核的调用
# -o 将所有的系统调用,放到文件里面
$starce -ff -o out nc -l 8080
# 1. 查看nc -l 8080所调用的内核命令,这里仅看一部分内容
...
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 3 # 创建一个socket并返回一个文件描述符3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
setsockopt(3, SOL_IPV6, IPV6_V6ONLY, [1], 4) = 0
bind(3, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0 # 为套接字分配名称
listen(3, 10) = 0 # 监听文件描述符3
fcntl(3, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 4 # 创建一个socket并返回一个文件描述符4
setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(4, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
# 为套接字分配名称
listen(4, 10) = 0 # 监听文件描述符4
fcntl(4, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
select(5, [3 4], [], NULL, NULL # 程序卡在select不向下继续执行了
# 2. 新开一个窗口,创建一个客户端
$ nc localhost 8080
# 3. 查看out文件的实时输出
$ tail -f out.XXXX
select(5, [3 4], [], NULL, NULL) = 1 (in [3])
accept(3, {sa_family=AF_INET6, sin6_port=htons(53522), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [128->28]) = 5
close(3) = 0
close(4) = 0
fcntl(5, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0
select(6, [0 5], [], NULL, NULL
# 从上面的结果看,能看到select继续往下走
# 并获取了文件描述符3的监听
# 关闭了文件描述符3,4。然后对文件描述符5进行了阻塞。
# 4. 在客户端发送一个hello发送给服务端
# 查看更新后的文件
select(6, [0 5], [], NULL, NULL) = 1 (in [5])
recvfrom(5, "hello\n", 8192, 0, NULL, NULL) = 6 # 从文件描述符5中读取hello
write(1, "hello\n", 6) = 6 # 向标准输出中输出1
select(6, [0 5], [], NULL, NULL # 进入轮询
# 5. 在服务端发送一个world发送给客户端
select(6, [0 5], [], NULL, NULL) = 1 (in [0])
read(0, "world\n", 8192) = 6 # 从标准输入流中读到world
fcntl(5, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK)
fcntl(5, F_SETFL, O_RDWR) = 0
sendto(5, "world\n", 6, 0, NULL, 0) = 6 # 发送world到文件描述符5
fcntl(5, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(5, F_SETFL, O_RDWR|O_NONBLOCK) = 0
select(6, [0 5], [], NULL, NULL # 继续轮询等待
网络通信的发展
初始阶段BIO,阻塞型IO
当程序作为服务端发起网络通信时,首先会创建一个socket并返回一个文件描述符fd,然后调用bind,调用listen,在进而调用accpet,最后等待客户端连接。
如果客户端,经历了3次握手之后,完成分配资源。
服务端的accpet会返回给客户端一个文件描述符4(递增+1),并调用read函数读取文件描述符4。
如果客户端没有发送数据内容,那么服务端将一直处于阻塞状态。
如果在阻塞的时候,新创建了一个客户端,与内核建立完成3次握手,此时服务端应当返回一个文件描述符给新的客户端。但是,由于服务端在阻塞,所以无法返回新的描述符给新的客户端。
弊端,长时间阻塞
多线程模型
为了解决BIO的阻塞情况,在服务端接受到链接请求后,并返回给一个文件描述符给客户端。然后,新启动一个线程调用read函数,来监听新的文件描述符的调用,此处是为阻塞。
当然,如果客户端越多启动的线程也随之越多。
线程的本质:
线程的创建,需要调用内核的clone,调用内核往往需要80软中断。开辟资源很慢,cpu负载比较高。
弊端,过多线程的创建
进阶阶段NIO,非阻塞型NIO
内核的更新,内核将文件描述符设置称为非阻塞的。
从启动流程,开始梳理。程序服务端启动,创建一个socket,然后调用bind,listen,accept。当内核程序将文件描述符设置为非阻塞后,服务端可以启动一个while循环,循环内accept(fd3)。
当客户端创建后,服务端返回一个文件描述符fd4给客户端,然后服务端循环读fd4,当读取不出内容时触发一个读空返回。
此时,如果有新的客户端创建,服务端收到信息返回一个新的文件描述符,然后进入到while循环中,循环读取文件内容。
弊端,如果有1w个客户端,但每一个都要调用一次read,每次都要触发80中断。
IO多路复用,select
内核中多了一个select的系统调用。服务端为了降低循环遍历read的系统调用,例如1w次降低为1次。服务端,此时每while一次仅需要调用一次select,遍历由内核来做并返回给服务端一个文件描述符,服务端只需要单独去读返回的文件描述符即可。内核来做O(n)的一万次遍历。
弊端:内核O(n),主动遍历
IO多路复用,poll
网卡和cpu的通信过程,当数据到达时会有高低电平,内核首先会将数据信息放到buffer中,然后触发cpu的中断,然后cpu根据中断回调Call Backup。
内核不再主动遍历1w个文件描述符,而是去查看有谁触发过中断,并把中断放在一个集合中,返回给服务端。
IO多路复用,epoll
服务端创建时,会创建一个socket得到一个文件描述符,然后bind,listen。此时,会调用epoll_create,内核空间会创建出一个文件描述符fd8,然后利用epoll_ctl将文件描述符fd6放到文件描述符fd8中,然后调用epoll_wait等待文件描述符fd8。
创建一个客户端后,accept接受到了文件描述符fd3,epoll_ctl将文件描述符fd3放入到文件描述符fd8中,然后继续进入到epoll_wait的等待中。
程序首先获得创建socket的文件描述符fd6,然后内核调用epoll得到文件描述符fd8,同时将fd8和fd6进程绑定,最后进入到epoll_wait的等待中。
当创建一个客户端后,建立完成tcp连接后,内核epoll机制通过fd6,它所关心事件accpet,创建客户端时会触发这个事件。此时,内核会将fd6挪到一个类似于数组当中,当发现是一个accpet,那么epoll就会接受这个客户端并返回一个文件描述符,fd3,epoll同时也会将fd3放入到内核当中。然后,继续wait8,一个是对fd3监听可读事件,一个是看fd6是否有accept事件。
epoll_ctl会将描述符常驻到内核的内存区域中,直到写释放关闭。