Linux_网络
内容
- 基本概念
- 了解接口socket
- 了解协议tcp/udp
- io复用
- select
- poll/epoll
分层模型
OSI:七层
TCP/IP协议族:四层
问题
- 为什么要分层?
- 解耦,分工。
- 网络:
- 互联网:网络和网络连接起来,就是互联网
- ipv4:是32位的,用“.”分成4个段,每个段是8位。每个段用1个10进制表示。
- ipv6:是128位的,用“:”分成8个段,每个段是16位。每个段用4个16进制表示。
- MAC地址:物理地址,又称硬件地址。
- 有了MAC地址,为什么还要有ip地址?寻址!
- 端口号:软件层次的概念。因为最终要达到进程的通信。
- 协议:
- tcp
- http
- ip
- udp
TCP编程流程
面试唯一要写代码的。
- 服务器端:
- 创建套接字socket()
- 所需地址:ip+port
- bind()
- listen()
- c = accept()
- recv()
- send()
- close()
- 创建套接字socket()
- 客户端:
- socket()
- connect()
- send()
- recv()
- close()
TCP
特点:
- 面向连接的
- 三次握手–在客户端connect()时
- 必须是三次
- 四次挥手–在任一方close时
- 有时可以三次
- 三次握手–在客户端connect()时
- 可靠的
- 应答确认
- 超时重传
- 乱序重排
- 去重
- 滑动窗口进行流量控制
- 流式服务
- 发送和接收的次数可能不一致。
- 连续多次发送的数据可能会被对方一次性收到。
- 起始末尾加标记
- send之后recv隔开
- 连续多次发送的数据可能会被对方一次性收到。
- 发送和接收的次数可能不一致。
- tcp是有状态的
- 开始closed
- listen–connecting(三次握手中)
- established(已完成握手)
- FIN_WAIT_1/2
- TIME_WAIT
- 可靠地终止TCP的连接
- 让迟来的报文在这一段时间被识别,即收集后丢弃,以防止误传给下一个使用该端口的连接。
问题:1、TCP/IP协议详解 卷一;2、UNIX网络编程 卷一
- 三次握手,四次挥手
- 应答确认、超时重传机制
- 乱序重排、去重、滑动窗口进行流量控制
- 什么是粘包?怎么解决?
- 中间转换状态的意义?TIME_WAIT状态的意义?
UDP
特点:
- 无连接的
- 没有严格意义上的服务端/客户端,只要知道ip:port就可以给对方sendto发消息。recvfrom谁的消息都可以收到,也可以收到不同终端的消息。
- 不可靠的
- 没有应答确认机制,发出后是否成功看天意。
- 数据报服务
- 如果send了很多信息,recv一次没收完,则剩下的数据丢包。
- UDP的send和recv也有缓冲区,但是其与收发的动作是一个整体,对应着发送/接收的那一次。
- 无状态的
UDP编程流程
在UDP场景中,其实没有严格意义上的服务端/客户端。因为编程流程很相似。
服务端
socket()
bind()
recvfrom()
sendto()
close()
客户端
socket()
【bind():可以绑定也可以不绑定】
recvfrom()
sendto()
close()
与tcp的区别
- 创建socket时的第二个参数TCP中是SOCK_STREAM,UDP中是SOCK_DGRAM(指datagrams)
1 | int sockfd = socket(AF_INET,SOCK_DGRAM,0); |
- recvfrom中多了两个参数,前四个参数与TCP一致,后面一个传入要接收哪个主机的sockaddr类型的指针,最后是传入这个sockaddr对象的长度的指针。为什么要传指针?因为UDP中某一端接收的数据可以来自不同主机,所以传入指针是要实时的记录谁在给接收端发数据。
- 同样的,sendto也多了两个参数,后面一个传入要发送到哪个的主机的sockaddr类型的指针(参数类型为
const struct sockaddr* dest_addr
),最后传入这个sockaddr
对象的长度大小,通常用sizeof
计算。相对于recvfrom
,为什么不传非const指针
了?因为:要么是服务端,sendto
的目的主机已经在recvfrom
收到客户端消息后明确;要么是客户端要sendto
的目的主机已经很明确地指定。
一个端口在开启TCP服务的同时,也可以开启UDP服务
我们常说:一个端口标识一个进程,但是对于不同的协议,两个进程居然可以绑定到同一端口!但是,知道为什么能这样做,再去想,这句话还是没毛病的。
只有一个端口,如何能够区分不同协议?这个区分工作通常是在应用层完成的。比如,一个进程是TCP服务端,另一个进程的UDP服务端。UDP报文过来了,该给谁;下一刻TCP数据过来了再给谁。端口虽然不能区分不同协议的数据报,但是我们最终都要把该数据包传到某个进程中去!两个进程同时监听到了端口的数据传来,此时这个数据报头已经加了其协议头,进程在去解析协议头时就清楚了该不该继续接收这个数据报,这样就自然的完成了对不同协议的区分。即:在应用层,每个连接就需要按照五元组来区分:{协议,源ip,源port,目的ip,目的port}。即使不区分,按照SOCK_STREAM或SOCK_DGRAM,报文类型不一样,根本就不适配,所以两者使用的端口在应用层来说是独立的。
http
- 浏览器解析到IP后,端口约定为80,connect三次握手建立TCP连接。
- 浏览器给服务器发送http请求报文。浏览器send
- 服务器给浏览器回复应答报文。浏览器recv
- 如果2、3步只发生一次,为短连接。发生了两次以上,则为长连接,之后不会很快断开。如此可以避免握手、挥手浪费的时间。
I/O复用
- select
- poll
- epoll
select
先观察其API
1 |
|
- 参数
- nfds参数通常被设置为select监听的所有文件描述符中的最大值加1,表示在fd_set集合中,我们关心的描述符的总数。为什么加1呢?因为文件描述符是从0开始计数的。nfds与fd_set容量大小不一样,容量大小指的是FD_SETSIZE,即fd_set容量大小是fd_set可容纳描述符的最大大小。
- readfds参数是select关心的读事件的集合;
- writefds参数是select关心的写事件的集合;
- exceptfds参数select关心的异常事件的集合;
- timeout参数设置select的超时时间。
- 返回值
- 集合中有事件就绪的描述符的个数
- 但是并没有告诉你具体是哪一个描述符就绪
fd_set结构体
1 |
|
其中,__FD_SETSIZE指出select可以关注的最大文件描述符个数,默认为1024。
__fd_mask被定义为long int的类型别称,long int在32位机占8个字节。
__NFDBITS计算的是1个__fd_mask元素所占用的位数,一个字节占8位,sizeof算出__fd_mask的字节数,相乘得其占用的bit大小。
接着,定义fds_bits,其是long int型的数组,数组大小为__FD_SETSIZE除以__NFDBITS。比如SETSIZE为1024位,NFDBITS是64位,则数组大小位1024/64=16。这里的计算主要是为了计算出数组的大小,以确定多大的数组可以正好容纳1024个位数,来记录文件描述符信息。
用到的宏函数
fd_set集合对于文件描述符的管理是按位进行的,而位只有0和1两种状态。
假如SETSIZE=1024,则可管理1024个文件描述符,如果文件描述符7有效,我们需要对位操作,使其位变为1。
由于位操作过于繁琐,select API中提供了一系列宏函数来方便我们访问、操作fd_set集合状态。
1 |
|
select编程思路
最好另外定义一个整型数组,其大小为我们预测将要出现的描述符的最多数目。用作我们存放描述符的容器。初始化时将数组值一律设为-1,表示容器中该位置还没有存放描述符。如果在某一时刻有一个描述符有了消息,我们就将该描述符数值覆盖到这个容器中第一个为-1的地方。
1 |
|
示例–TCP服务使用select处理多个套接字
1 | int main() |
场景情况:如果客户端与select服务端已建立连接,而客户端进程结束,select会一直阻塞、未感知吗?–不会。
因为客户端的进程结束,也算是一种读事件,相当于通知服务端该套接字连接结束了。那么服务端recv会返回0,达到关闭该套接字的条件,关闭后,别忘了在fds容器中删除掉该描述符。
如果忘记了close该套接字,且忘了fds_del该描述符,那么如果客户端结束进程,服务端就会一直打印"client close",因为select一直在探测此描述符有无读事件,若该套接字连接关闭,那么此描述符一直有读事件,recv返回0,由于没有fds_del,每次都会关注,所以每次都会打印"client close"。
poll
可以理解为加强版的select。
先观察其API
1 |
|
- fds参数是一个pollfd结构类型的指针,可指向一段连续空间(数组),因此很灵活,大小可按需声明。它可以指定我们感兴趣的文件描述符上发生的可读、可写和异常等事件。定义如下
1 | struct pollfd |
- 其中,fd成员指定文件描述符。
- events成员告诉poll监听fd上的哪些事件类型,他可以是一系列事件类型的按位或。常见的事件类型有:POLLIN(数据可读)、POLLOUT(数据可写)。
- revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件。
poll编程
1 | void poll_fds_init(struct pollfd* fds) |
与select的一处细节区别:
每次select除了监测fd_set有效描述符上有无事件,其次还将没有事件的描述符从fd_set移除(其实是将该描述符对应在fd_set上的位进行置0操作)(这样就得每次select之前都要重新注册一遍我们关注的描述符(即用户和内核共同操作FD_SET、FD_CLR等)。),然后下面过滤有事件的描述符时,只要找到fd_set集合哪个位是1状态即就找到了有事件产生的描述符。
而poll的用法是:用户只管注册events,实际上的有无事件由内核来进行对revents的填充,以此来更好地区别该描述符是否有事件产生。这样,就不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events了。我们只要把要关心的描述符的fd置成非-1,以及管理好要关心的哪些事件类型events即可。
与select相比的优点
- 可以监听的描述符的最大数目可以超过1024个,大小按需自拟。
- 可以监听的事件类型数目变多、变细了,更强大了。
- 不用在每次poll之前重新注册一遍我们关注的描述符的结构体里的events了
epoll
epoll是Linux特有的I/O复用函数。
epoll的使用实际上不是单独的API,而是有一组函数来完成。三个函数:
1 |
|
内核事件表
epoll的最大优势在于处理描述符特别多的情况,相比轮询方式:
- 如果用传统的select、poll单纯的顺序轮询方法监测就绪的描述符,那么性能会很低下的。
- 而且虽然select、poll能顺利查出有几个描述符上有事件产生,但是是哪个描述符?并没有告诉我们,所以又得浪费一轮时间来查找产生时间是哪个描述符。