OpsDev 团队自从九月中旬开启技术分享活动以来,受到了广大技术爱好者的好评,收效显著。第一期公开课由李钢老师开讲,内容为“应用编程基础课”,分为 5 节课,本文为本期的第二节课,重点讲解网络基础编程,包括网络结构、TCP 编程模型和 IO 模型。现整理成文分享出来供大家一起学习。
前言
本人从事 linux 下 web 编程多年,最近有幸给组内同学做培训,希望能够给大家介绍下自己这些年在应用编程方面的经验,今天给大家介绍以下网络编程方面需要掌握的基础知识。
1 网络分层模型
先看一张图:
从左到右向,分别是:
1.OSI 七层模型
2.TCP/IP 四层模型
3.应用程序实现部分和内核实现部分
这里要认识到的是,我们最常用 TCP 的网络处理部分,都是由内核来完成的。
2 TCP 服务端和客户端编程模型
TCP 连接创建和断开
TCP 创建连接需要三次握手,而断开连接需要四次挥手,如图:
这张图清晰的说明了连接的建立、数据发送以及断开连接时所对应的编程函数,另外还有相应的 TCP 状态转换。
服务端客户端编程函数
由此可见,服务端编程用到的主要函数为:
1.socket:创建一个 socket,返回的文件描述符 fd 之后用于 bind 和 listen
2.bind:绑定 socket 和 ip+port
3.listen:调用后,服务端状态变为 LISTEN,可以接收网络连接
4.accept:函数在连接建立后返回一个 connfd,对这个文件描述符的读写就是在做网络接收和发送
5.read:网络对端发送来的数据会放到内核的接收缓冲区,read 就是从这个缓冲区中读取数据到应用程序
6.write:应用程序要发送数据到网络对端时,调用此函数,会现将数据写到内核的发送缓冲区中,之后内核会负责将数据发送给网络对端
7.close:关闭连接
关于服务端的用于连接的 fd 和用于读写的 fd,请见下图:
客户端编程用到的函数为:
1.socket:创建一个 socket,之后用于连接服务器,做数据读写用
2.connect:发起到服务端的链接,返回时 TCP 三次握手完成
3.write:同服务端的 write
4.read:同服务端的 read
5.close:关闭连接
几个概念
backlog
内核中会维护两个队列:
1.未完成连接的队列: 服务端收到客户端的连接请求(SYNC),在三次握手完成前,会放到这个队列中
2.完成连接的队列:完成三次握手后就创建了一个 TCP 连接,这个连接会放到这个队列中
linux 的 man listen 中说:
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow
我理解,backlog 定义了未完成连接的队列的最大长度
RTT
这个概念常常听到,请见图:
RTT 的定义是 Round-Trip Time,即数据包的往返时延
TCP Stream
我们通常说 TCP 是流式的,这是什么样的概念呢?
对应一个 TCP 连接,内核会给这个连接分配一个发送缓冲和接收缓冲,我们的应用程序对这来两个缓冲区的读写就是在做网络数据的接收和发送。
而数据在网络上的传输是内核自己在维护的,当发送缓冲区中有数据后,内核就把这些数据发送给对端;同样的,当对端有数据过来时,内核会把它放到接收缓冲区中,等待应用程序的读写。
这样的发送和接收数据的过程,就像水流一样,所以我们说 TCP 是流式的。
TCP 状态转换
TCP 定义了很多状态,这些状态之间的转换关系如下图:
这些状态都记住有难度,需要时查下就好了。
3 IO 模型
网络操作就是 IO 操作,而且网络的 IO 是最慢的一种了,网络编程的很大难点就是妥善的处理好这一块的问题。
Unix 定义了多种 IO 操作模型,分别是:
1.阻塞 IO
2.非阻塞 IO
3.IO 多路复用
4.信号驱动 IO
5.异步 IO
阻塞 IO
这里有一点非常重要的概念要先说明下,那就是阻塞的是什么?
首先要记住:数据在网络上的传输完全是内核在控制的,应用程序中的 read 和 write 只是在读写接收缓冲和发送缓冲。
读阻塞:调用 read 时,如果接收缓冲区中没有数据,那么就会产生阻塞
写阻塞:调用 write 时,如果发送缓冲区中的数据没有发送出去,那么就会产生阻塞
那么如果没有产生阻塞,那么两者的执行时间为:
read 的执行时间为:将内核接收缓冲区的内容拷贝到用户空间中的应用程序缓冲区
write 的执行时间为:将用户空间中的应用程序缓冲区的内容拷贝到内核的发送缓冲区
非阻塞 IO
理解了导致阻塞的原因,那么非阻塞就非常好理解了:
当内核缓冲区无法读写时,read 和 write 就会返回 EWOULDBLOCK,应用程序就需要过会儿再来操作。
光是这样,还不能实现高性能的网络程序,这是因为我们无法判断应该什么时间再来做读写操作。
如果一直循环读写,那么 CPU 占用会很居高不下;如果 sleep 一段时间,那么多长时间合适呢?
所以,如果想开发高性能的网络程序,我们还需要别的武器。
IO 多路复用
这是操作系统提供的一种通知机制,告诉应用程序何时可以做读写操作:
不同操作系统提供了不同的编程接口,一个非常有名的库 libevent 就是对这些库的一个统一接口封装。
IO 多路复用也是现在用的最多的一种高性能网络服务器的 IO 处理模型,例如 Nginx
信号驱动 IO
不同于 IO 多路复用,操作系统用信号的方式告诉应用程序何时可以做读写操作:
异步 IO
最后这一种我没有用过,从概念上理解,相当于操作系统将数据做完用户空间和内核空间的复制后,才会通知应用程序:
4 面对如此众多的服务器
上面这些,都是笔者编程这些年,觉得非常受用的基础知识。正确的认识这些知识,很多问题你都可以自己想明白了。
笔者也是在不断学习中,如果有错误的地方,还望指正,我们共同进步,谢谢!
本文转载自公众号 360 云计算(ID:hulktalk)。
原文链接:
https://mp.weixin.qq.com/s/dtmosNh_ej3nGC8suuV6qA
评论