不可不知的Socket和TCP连接过程
副标题[/!--empirenews.page--]
本文主要说明的是TCP连接过程中,各个阶段对套接字的操作,希望能对没有网络编程基础的人理解套接字是什么、扮演的角色有所帮助。如发现错误,敬请指出。 一. 背景 1.完整的套接字格式{protocol,src_addr,src_port,dest_addr,dest_port}。 这常被称为套接字的五元组。其中protocol指定了是TCP还是UDP连接,其余的分别指定了源地址、源端口、目标地址、目标端口。但是这些内容是怎么来的呢? 2.TCP协议栈维护着两个socket缓冲区:send buffer和recv buffer。 要通过TCP连接发送出去的数据都先拷贝到send buffer,可能是从用户空间进程的app buffer拷入的,也可能是从内核的kernel buffer拷入的,拷入的过程是通过send()函数完成的,由于也可以使用write()函数写入数据,所以也把这个过程称为写数据,相应的send buffer也就有了别称write buffer。不过send()函数比write()函数更有效率。 最终数据是通过网卡流出去的,所以send buffer中的数据需要拷贝到网卡中。由于一端是内存,一端是网卡设备,可以直接使用DMA的方式进行拷贝,无需CPU的参与。也就是说,send buffer中的数据通过DMA的方式拷贝到网卡中并通过网络传输给TCP连接的另一端:接收端。 当通过TCP连接接收数据时,数据肯定是先通过网卡流入的,然后同样通过DMA的方式拷贝到recv buffer中,再通过recv()函数将数据从recv buffer拷入到用户空间进程的app buffer中。 大致过程如下图: 3.两种套接字:监听套接字和已连接套接字。 监听套接字是在服务进程读取配置文件时,从配置文件中解析出要监听的地址、端口,然后通过socket()函数创建的,然后再通过bind()函数将这个监听套接字绑定到对应的地址和端口上。随后,进程/线程就可以通过listen()函数来监听这个端口(严格地说是监控这个监听套接字)。 已连接套接字是在监听到TCP连接请求并三次握手后,通过accept()函数返回的套接字,后续进程/线程就可以通过这个已连接套接字和客户端进行TCP通信。 为了区分socket()函数和accept()函数返回的两个套接字描述符,有些人使用listenfd和connfd分别表示监听套接字和已连接套接字,挺形象的,下文偶尔也这么使用。 下面就来说明各种函数的作用,分析这些函数,也是在连接、断开连接的过程。 二. 连接的具体过程分析 如下图: 2.1 socket()函数 socket()函数的作用就是生成一个用于通信的套接字文件描述符sockfd(socket() creates an endpoint for communication and returns a descriptor)。这个套接字描述符可以作为稍后bind()函数的绑定对象。 2.2 bind()函数 服务程序通过分析配置文件,从中解析出想要监听的地址和端口,再加上可以通过socket()函数生成的套接字sockfd,就可以使用bind()函数将这个套接字绑定到要监听的地址和端口组合"addr:port"上。绑定了端口的套接字可以作为listen()函数的监听对象。 绑定了地址和端口的套接字就有了源地址和源端口(对服务器自身来说是源),再加上通过配置文件中指定的协议类型,五元组中就有了其中3个元组。即: {protocal,src_addr,src_port} 但是,常见到有些服务程序可以配置监听多个地址、端口实现多实例。这实际上就是通过多次socket()+bind()系统调用生成并绑定多个套接字实现的。 2.3 listen()函数和connect()函数 顾名思义,listen()函数就是监听已经通过bind()绑定了addr+port的套接字的。监听之后,套接字就从CLOSE状态转变为LISTEN状态,于是这个套接字就可以对外提供TCP连接的窗口了。 而connect()函数则用于向某个已监听的套接字发起连接请求,也就是发起TCP的三次握手过程。从这里可以看出,连接请求方(如客户端)才会使用connect()函数,当然,在发起connect()之前,连接发起方也需要生成一个sockfd,且使用的很可能是绑定了随机端口的套接字。既然connect()函数是向某个套接字发起连接的,自然在使用connect()函数时需要带上连接的目的地,即目标地址和目标端口,这正是服务端的监听套接字上绑定的地址和端口。同时,它还要带上自己的地址和端口,对于服务端来说,这就是连接请求的源地址和源端口。于是,TCP连接的两端的套接字都已经成了五元组的完整格式。 2.3.1 深入分析listen() 再来细说listen()函数。如果监听了多个地址+端口,即需要监听多个套接字,那么此刻负责监听的进程/线程会采用select()、poll()的方式去轮询这些套接字(当然,也可以使用epoll()模式),其实只监控一个套接字时,也是使用这些模式去轮询的,只不过select()或poll()所感兴趣的套接字描述符只有一个而已。 不管使用select()还是poll()模式(至于epoll的不同监控方式就无需多言了),在进程/线程(监听者)监听的过程中,它阻塞在select()或poll()上。直到有数据(SYN信息)写入到它所监听的sockfd中(即recv buffer),监听者被唤醒并将SYN数据拷贝到用户空间中自己管理的app buffer中进行一番处理,并发送SYN+ACK,这个数据同样需要从app buffer中拷入send buffer(使用send()函数)中,再拷入网卡传送出去。这时会在连接未完成队列中为这个连接创建一个新项目,并设置为SYN_RECV状态。然后再次使用select()/poll()方式监控着套接字listenfd,直到再次有数据写入这个listenfd中监听者才被唤醒,如果这次写入的数据是ACK信息,则将数据拷入到app buffer中进行一番处理后,把连接未完成队列中对应的项目移入连接已完成队列,并设置为ESTABLISHED状态,如果这次接收的不是ACK,则肯定是SYN,也就是新的连接请求,于是和上面的处理过程一样,放入连接未完成队列。这就是监听者处理整个TCP连接的循环过程。 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |