我相信大家刚开始学socket的时候,都跟我一样。
云里雾里的,对socket的概念很模糊。
这篇文章我打算从一个初学者的角度开始聊起,让大家了解下我眼里的socket是什么以及socket的原理和内核实现。
socket的概念
故事要从一个插头说起。
插头与插座
当我将插头插入插座,那看起来就像是将两者连起来了。
风扇与电力系统建立"连接"
而插座的英文,又叫socket
。
巧了,我们程序员搞网络编程时也会用到一个叫socket
的东西。
其实两者非常相似。通过socket
,我们可以与某台机子建立"连接",建立"连接"的过程,就像是将插口插入插槽一样。
大概概念是了解了,但我相信各位对socket
其实还是很模糊。
我们从大家最熟悉的使用场景开始说起。
socket的使用场景
我们想要将数据从A电脑的某个进程发到B电脑的某个进程。
这时候我们需要选择将数据发过去的方式,如果需要确保数据要能发给对方,那就选可靠的TCP
协议,如果数据丢了也没关系,看天意,那就选择不可靠的UDP
协议。
初学者毫无疑问,首选TCP
。
TCP是什么
那这时候就需要用socket
进行编程。
于是第一步就是创建个关于TCP的socket
。就像下面这样。
sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
这个方法会返回socket_fd
,它是socket文件的句柄,是个数字,相当于socket的身份证号。
得到了socket_fd
之后,对于服务端,就可以依次执行bind()
, listen()
, accept()
方法,然后坐等客户端的连接请求。
对于客户端,得到socket_fd
之后,你就可以执行connect()
方法向服务端发起建立连接的请求,此时就会发生TCP三次握手。
握手建立连接流程
连接建立完成后,客户端可以执行send()
方法发送消息,服务端可以执行recv()
方法接收消息,反过来,服务器也可以执行send()
,客户端执行recv()
方法。
到这里为止,就是我们大部分程序员最熟悉的使用场景。
socket的设计
现在,socket我们见过,也用过,但对大部分程序员来说,它是个黑盒。
那既然是黑盒,我们索性假设我们忘了socket。重新设计一个内核网络传输功能。
网络传输,从操作上来看,无非就是,发数据和远端之间互相收发数据。也就是对应着写数据和读数据。
读写收发
但显然,事情没那么简单。
这里还有两个问题。
第一个是,接收端和发送端可能不止一个,因此我们需要一些信息做下区分,这个大家肯定很熟悉,可以用IP和端口。IP用来定位是哪台电脑,端口用来定位是这台电脑上的哪个进程。
第二个是,发送端和接收端的传输方式有很多区别,可以是可靠的TCP协议
,也可以是不可靠的UDP协议
,甚至还需要支持基于icmp协议
的ping命令
。
sock是什么
写过代码的都知道,为了支持这些功能,我们需要定义一个数据结构去支持这些功能。
这个数据结构,叫sock
。
为了解决上面的第一个问题,我们可以在sock
里加入IP和端口字段。
sock加入IP和端口字段
而第二个问题,我们会发现这些协议虽然各不相同,但还是有一些功能相似的地方,比如收发数据时的一些逻辑完全可以复用。按面向对象编程的思想,我们可以将不同的协议当成是不同的对象类(或结构体),将公共的部分提取出来,通过"继承"的方式,复用功能。
基于各种sock实现网络传输功能
于是,我们将功能重新划分下,定义了一些数据结构。
继承sock的各类sock
sock
是最基础的结构,维护一些任何协议都有可能会用到的收发数据缓冲区。
inet_sock
特指用了网络传输功能的sock
,在sock
的基础上还加入了TTL
,端口,IP地址这些跟网络传输相关的字段信息。说到这里大家就懵了,难道还有不是用网络传输的?有,比如Unix domain socket
,用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈。这是个非常有用的东西,我以后一定讲讲(画饼)。
inet_connection_sock
是指面向连接的sock
,在inet_sock
的基础上加入面向连接的协议里相关字段,比如accept队列
,数据包分片大小,握手失败重试次数等。虽然我们现在提到面向连接的协议就是指TCP,但设计上linux需要支持扩展其他面向连接的新协议,
tcp_sock
就是正儿八经的tcp协议专用的sock
结构了,在inet_connection_sock
基础上还加入了tcp特有的滑动窗口、拥塞避免等功能。同样udp协议也会有一个专用的数据结构,叫udp_sock
。
好了,现在有了这套数据结构,我们将它们跟硬件网卡对接一下,就实现了网络传输的功能。