Socket介绍
不知道大家有没有想过网络通信到底是什么东西,为什么两个计算机之间可以通信?
其实本质上就是两个计算机之间的数据交换,而这个数据交换的过程就是通过socket
来完成的。
大家都知道现代网络分层模型有7层模型、5层模型等等。就拿八股文中常问的5层模型举例,分为:物理层、数据链路层、网络层、传输层、应用层。这些模型都是为了更好的管理网络通信,而socket
就是在应用层和传输层之间的一个抽象层,它把复杂的传输层抽象成了一个简单的接口,供应用层调用。这样应用层就不用太过于关注传输层实现,只需要简单的调用几个接口即可实现网络通信的功能。类似编程中的门面模式。
socket
的英文意思是“插座”,想像一下,我们在家里插上电源,就可以使用电器了。而socket
也是一样的道理,我们在应用层调用socket
接口,就可以使用网络通信了。
连接与通信的过程
说完socket
是个啥,这里就来说说socket
的通信过程。
上文说到socket
是一个介于应用层和传输层之间的一个抽象层,它能够简化网络通信。假设我们现在有两台计算机,一台是服务端,一台是客户端。两个计算机都可以各自创建并维护一个socket
,来进行通信。
服务端
服务端首先调用socket()
方法,这个方法有两个参数,第一个参数是domain
,第二个参数是type
。domain
是指定协议族,type
是指定协议类型。这里的协议族指的是网络协议,比如AF_INET
就是IPv4协议族,AF_INET6
就是IPv6协议族。而协议类型指的是传输协议,比如SOCK_STREAM
就是TCP协议,SOCK_DGRAM
就是UDP协议。这里的socket()
方法就是创建一个socket
,并且指定了协议族和协议类型。
创建好之后,接着调用bind()
方法来绑定服务端主机ip地址和端口。绑定端口的作用是内核收到数据包之后,可以根据端口号找到对应的socket
。绑定ip的作用是因为一个计算机通常有多个网卡,每个网卡的ip都不同,我们可以指定socket
只监听对应网卡的数据。
前置工作做好后,接着调用listen()
方法来进行监听。一般咱们要看一个网络程序有没有启动,都是通过终端输入netstat
命令来查看的。这个命令本质上也是看端口是否被监听。
最后调用accept()
方法来接收客户端的连接。如果有客户端连接过来,内核就会把相关的数据转发到对应的应用程序上,如果没有的话就会一直阻塞。
客户端
客户端首先也是调用socket()
方法来创建一个socket,这里也是指定了协议族和协议类型。
创建好socket之后,接着调用connect()
方法来连接服务端。这里的connect()
方法有三个参数,第一个参数是socket
,第二个参数是服务端的ip地址,第三个参数是服务端的端口号。这里的connect()
方法会向服务端发送一个SYN
包,服务端收到之后会回复一个SYN+ACK
包,客户端再回复一个ACK
包,这样就完成了三次握手,建立了连接。万众瞩目的tcp三次握手就是这个过程。
建立连接之后,服务端和客户端就可以通过write()
和read()
方法来进行通信了。
这里补充说明下,服务端的内核会维护两个队列。分别是TCP半连接队列和TCP全连接队列。前者主要保存还未完成三次握手的连接,后者主要保存已经完成三次握手的连接。
服务端调用accept()
方法会从TCP全连接队列取出一个连接返回给应用程序。后续客户端和服务端通信都是使用这个连接。
socket底层原理
到这里大家有没有发现,socket
的通信过程和文件读写的过程很像。其实socket
的底层原理就是文件读写。我们知道,进程里面有一个文件描述符数组,这个数组里面保存了所有打开的文件描述符。而socket
也是一个文件描述符,所以socket
也会保存在这个数组里面。该数组里列出这个进程打开的所有文件的文件描述符。数组的下标是文件描述符,是一个整数,而数组的内容是一个指针,指向内核中所有打开的文件的列表,也就是说内核可以通过文件描述符找到对应打开的文件。
然后每个文件都有一个 inode,Socket 文件的 inode 指向了内核中的 Socket 结构,在这个结构体里有两个队列,分别是发送队列和接收队列,这个两个队列里面保存的是一个个 struct sk_buff
,用链表的组织形式串起来。
sk_buff
可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。
你可能会好奇,为什么全部数据包只用一个结构体来描述呢?协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率。
于是,为了在层级之间传递数据时,不发生拷贝,只用 sk_buff
一个结构体来描述所有的网络包,那它是如何做到的呢?是通过调整 sk_buff
中 data 的指针,比如:
- 当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加
skb->data
的值,来逐步剥离协议首部。 - 当要发送报文时,创建
sk_buff
结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少skb->data
的值来增加协议首部。