Socket与TCP的关系

前言

相信大家对于TCP已经非常熟悉了,学习过计算机网络的同学对于它的连接和断开流程应该已经烂熟于心了吧。

那么Socket是什么?
Socket是应用层与TCP/IP协议簇通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
Socket

两种套接字

1. 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中。
流程如下图:
整体流程

2. 监听套接字和已连接套接字

1)监听套接字

在服务进程读取配置文件时,从配置文件中解析出要监听的地址、端口,然后通过socket()函数创建的,然后再通过bind()函数将这个监听套接字绑定到对应的地址和端口上。随后,进程/线程就可以通过listen()函数来监听这个端口(严格的来说监控这个监听套接字)
在Java中socket变成的代码实现:

// 返回的server可以认为是监听套接字
ServerSocket server = new ServerSocket(8888);
->
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {setImpl();if (port < 0 || port > 0xFFFF)throw new IllegalArgumentException("Port value out of range: " + port);if (backlog < 1)backlog = 50;try {bind(new InetSocketAddress(bindAddr, port), backlog);} catch(SecurityException e) {close();throw e;} catch(IOException e) {close();throw e;}
}
->
public void bind(SocketAddress endpoint, int backlog) throws IOException {if (isClosed())throw new SocketException("Socket is closed");if (!oldImpl && isBound())throw new SocketException("Already bound");if (endpoint == null)endpoint = new InetSocketAddress(0);if (!(endpoint instanceof InetSocketAddress))throw new IllegalArgumentException("Unsupported address type");InetSocketAddress epoint = (InetSocketAddress) endpoint;if (epoint.isUnresolved())throw new SocketException("Unresolved address");if (backlog < 1)backlog = 50;try {SecurityManager security = System.getSecurityManager();if (security != null)security.checkListen(epoint.getPort());// 绑定地址端口getImpl().bind(epoint.getAddress(), epoint.getPort());// 进行监听getImpl().listen(backlog);bound = true;} catch(SecurityException e) {bound = false;throw e;} catch(IOException e) {bound = false;throw e;}
}

在bind方法中创建了监听套接字。

2)已连接套接字

在监听到TCP连接请求并三次握手后,通过accept()函数返回的套接字,后续进程/线程就可以通过这个已连接套接字和客户端进行TCP通信。
为了区分socket()函数和accept()函数返回的两个套接字描述符,有些人使用listenfd和connfd分别表示监听套接字和已连接套接字,挺形象的,下文偶尔也这么使用。
在Java中socket编程体现:

//调用accept方法返回一个已连接套接字
Socket client = server.accept();

注意上面说的都是操作系统层面上的套接字,而不是抽象出来的Socket接口服务。
下面就来说明各种函数的作用,分析这些函数,也是在连接、断开连接的过程。

具体流程

流程图:
在这里插入图片描述

1. Socket()函数

socket()函数的作用就是生成一个用于通信的套接字文件描述符sockfd(socket() creates an endpoint for communication and returns a descriptor)。这个套接字描述符可以作为稍后bind()函数的绑定对象。

Socket socket = new Socket();

2. bind()函数

服务程序通过分析配置文件,从中解析出想要监听的地址和端口,在加上可以通过socket()函数生成的套接字sockfd,就可以使用bind()函数将这个套接字绑定到要监听的地址和端口组合“addr:port”上。绑定了端口的套接字可以作为listen()函数的监听对象。
绑定了地址和端口的套接字就有了源地址和源端口(对服务自身来说是源),再加上通过配置文件中指定的协议类型,五元组中就有了其中三个。即:
{protocal, src_addr, src_port}
但是,常见到有些服务程序可以配置监听多个地址、端口实现多实例。这实际上就是通过多次socket() + bind()系统调用生成并绑定多个套接字实现的。

InetSocketAddress address = new InetSocketAddress(host, port);
socket.bind(address);

3. listen()和connect()

listen()函数就是监听已经通过bind()绑定了addr+port的套接字的。监听之后,套接字就从CLOSE状态转变为LISTEN状态,于是这个套接字就可以对外提供TCP连接的窗口了。
而connect()函数则用于向某个已监听的套接字发起连接请求,也就是发起TCP的三次握手过程。从这里可以看出,连接请求方(如客户端)才会使用connect()函数,当然,在发起connect()之前,连接发起方也需要生成一个sockfd,且使用的很可能是绑定了随机端口的套接字(可以看一下自己计算机的端口使用情况,基本都是一些随机的端口)。既然connect()函数是向某个套接字发起连接的,自然在使用connect()函数时需要带上连接的目的地,即目标地址和目标端口,这正是服务端的监听套接字上绑定的地址和端口。同时,它还要带上自己的地址和端口,对于服务端来说,这就是连接请求的源地址和源端口。于是,【TCP连接的两端的套接字都已经成了五元组的完整格式。】

有关listen的代码前面已经给出client.connect(address);所有的步骤Java中socket类都有相对应的方法

4. 深入分析listen函数

再来细说listen()函数。如果监听了多个地址+端口,即需要监听多个套接字,那么此刻负责监听的进程/线程会采用select()poll()的方式去轮询这些套接字(当然,也可以使用epoll()模式),其实只监控一个套接字时,也是使用这些模式去轮询的,只不过select()或poll()所感兴趣的套接字描述符只有一个而已。

【下面的内容很重要!!!!!】

第一步:客户端发起连接

不管使用select()还是poll()模式(至于epoll的不同监控方式就无需多言了),在进程/线程(监听者)监听的过程中,它阻塞在select()或poll()上。直到有数据(SYN信息)写入到它所监听的sockfd中(即recv buffer)。

第二步:服务端发送syn+ack

内核被唤醒(注意不是app进程被唤醒,因为TCP三次握手和四次挥手是在【内核空间】由内核完成的,不涉及用户空间)并将SYN数据拷贝到kernel buffer中进行一番处理(比如判断SYN是否合理),并准备SYN+ACK数据,这个数据需要从kernel buffer中拷入send buffer中,再拷入网卡传送出去。这时会在连接未完成队列(syn queue)中为这个连接创建一个新项目,并设置为SYN_RECV状态(这个连接状态)。

第三步:客户端发出确认ack

然后再次使用select()/poll()方式监控着套接字listenfd,直到再次有数据写入这个listenfd中,内核再次被唤醒,如果这次写入的数据是ACK信息,表示是某个客户端对服务端内核发送的SYN的回应,于是将数据拷入到kernel buffer中进行一番处理后,把连接未完成队列中对应的项目移入连接已完成队列(accept queue/established queue),并设置为ESTABLISHED状态。如果这次接收的不是ACK,则肯定是SYN,也就是新的连接请求,于是和上面的处理过程一样,放入连接未完成队列。
对于已经放入已完成队列中的连接,将等待内核通过accept()函数进行消费(由用户空间进程发起accept()系统调用,由内核完成消费操作),只要经过accept()过的连接,连接将从已完成队列中移除,也就表示TCP已经建立完成了,两端的用户空间进程可以通过这个连接进行真正的数据传输了,直到使用close()或shutdown()关闭连接时的4次挥手,中间再也不需要内核的参与。这就是监听者处理整个TCP连接的循环过程。

也就是说,listen()函数还维护了两个队列:【连接未完成队列(syn queue)】(有一种攻击就是半连接攻击)和【连接已完成队列(accept queue)】。当监听者接收到某个客户端发来的SYN并回复了SYN+ACK之后,就会在未完成连接队列的尾部创建一个关于这个客户端的条目,并设置它的状态为SYN_RECV。显然,这个条目中必须包含客户端的地址和端口相关信息(可能是hash过的,我不太确定)。当服务端再次收到这个客户端发送的ACK信息之后,监听者线程通过分析数据就知道这个消息是回复给未完成连接队列中的哪一项的,于是将这一项移入到已完成连接队列,并设置它的状态为ESTABLISHED,最后等待内核使用accept()函数来消费接收这个连接。从此开始,内核暂时退出舞台,直到4次挥手。

5. accept()方法

accpet()函数的作用是读取已完成连接队列中的第一项(读完就从队列中移除),并对此项生成一个用于后续连接的套接字描述符,假设使用connfd来表示。有了新的连接套接字,工作进程/线程(称其为工作者)就可以通过这个连接套接字和客户端进行数据传输,而前文所说的监听套接字(sockfd)则仍然被监听者监听。

6. TCP与Socket的关系

先明确一点,每个tcp连接的两端都会关联一个套接字和该套接字指向的文件描述符。

前面说过,当服务端收到了ack消息后,就表示三次握手完成了,表示和客户端的这个tcp连接已经建立好了。连接建立好的一开始,这个tcp连接会放在listen()打开的established queue队列中等待accept()的消费。这个时候的tcp连接在服务端所关联的套接字是listen套接字和它指向的文件描述符。
当established queue中的tcp连接被accept()消费后,这个tcp连接就会关联accept()所指定的套接字,并分配一个新的文件描述符。也就是说,经过accept()之后,这个连接和listen套接字已经没有任何关系了。
套接字

换句话说,连接还是那个连接,只不过服务端偷偷地换掉了这个tcp连接所关联的套接字和文件描述符,而客户端并不知道这一切。但这并不影响双方的通信,因为数据传输是基于连接而不是基于套接字的,只要能从文件描述符中将数据放入tcp连接这根"管道"里,数据就能到达另一端。
实际上,并不一定需要accept()才能进行tcp通信,因为在accept()之前连接就以建立好了,只不过它关联的是listen套接字对应的文件描述符,而这个套接字只识别三次握手和四次挥手涉及到的数据,而且这个套接字中的数据是由操作系统内核负责的。可以想像一下,只有listen()没有accept()时,客户端不断地发起connect(),服务端将一直将建立仅只连接而不做任何操作,直到listen的队列满了。

7. send()和recv()

send()函数是将数据从app buffer复制到send buffer中(当然,也可能直接从内核的kernel buffer中复制),recv()函数则是将recv buffer中的数据复制到app buffer中。当然,对于tcp套接字来说,更多的是使用write()和read()函数来发送、读取socket buffer数据,这里使用send()/recv()来说明仅仅只是它们的名称针对性更强而已。
这两个函数都涉及到了socket buffer,但是在调用send()或recv()时,复制的源buffer中是否有数据、复制的目标buffer中是否已满而导致不可写是需要考虑的问题。不管哪一方,只要不满足条件,调用send()/recv()时进程/线程会被阻塞(假设套接字设置为阻塞式IO模型)。当然,可以将套接字设置为非阻塞IO模型,这时在buffer不满足条件时调用send()/recv()函数,调用函数的进程/线程将返回错误状态信息EWOULDBLOCK或EAGAIN。buffer中是否有数据、是否已满而导致不可写,其实可以使用select()/poll()/epoll去监控对应的文件描述符(对应socket buffer则监控该socket描述符),当满足条件时,再去调用send()/recv()就可以正常操作了。还可以将套接字设置为信号驱动IO或异步IO模型,这样数据准备好、复制好之前就不用再做无用功去调用send()/recv()了。

8. close()和shutdown()

通用的close()函数可以关闭一个文件描述符,当然也包括面向连接的网络套接字描述符。当调用close()时,将会尝试发送send buffer中的所有数据。但是close()函数只是将这个套接字引用计数减1,就像rm一样,删除一个文件时只是移除一个硬链接数,只有这个套接字的所有引用计数都被删除,套接字描述符才会真的被关闭,才会开始后续的四次挥手中。对于父子进程共享套接字的并发服务程序,调用close()关闭子进程的套接字并不会真的关闭套接字,因为父进程的套接字还处于打开状态,如果父进程一直不调用close()函数,那么这个套接字将一直处于打开状态,将一直进入不了四次挥手过程。
而shutdown()函数专门用于关闭网络套接字的连接,和close()对引用计数减一不同的是,它直接掐断套接字的所有连接,从而引发四次挥手的过程。可以指定3种关闭方式:
1.关闭写。此时将无法向send buffer中再写数据,send buffer中已有的数据会一直发送直到完毕。

2.关闭读。此时将无法从recv buffer中再读数据,recv buffer中已有的数据只能被丢弃。

3.关闭读和写。此时无法读、无法写,send buffer中已有的数据会发送直到完毕,但recv buffer中已有的数据将被丢弃。
无论是shutdown()还是close(),每次调用它们,在真正进入四次挥手的过程中,它们都会发送一个FIN。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/598829.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

车位关键点检测-车位识别-车辆识别检测(教程+代码)

车位关键点检测、车位识别和车辆识别检测是现代智能交通系统中的重要技术&#xff0c;它们在提高停车管理效率和交通流畅度方面起着重要作用。以下是对这三个技术的分点阐述&#xff1a; 1. 车位关键点检测&#xff1a; 车位关键点检测是指通过计算机视觉技术&#xff0c;自动…

Docker中镜像的相关操作

1.辅助操作 docker version&#xff1a;用查看docker客户端引擎和server端引擎版本信息。 docker info&#xff1a;用来查看docker引擎的详细信息。 docker --help&#xff1a;用来查看帮助信息。 2.镜像Image docker images&#xff1a;查看当前本地仓库中存在哪些镜像。 …

解决uniapp打包成apk后uni.getStorageSync获取不到值

uniapp写的项目&#xff0c;在hbuilderx中云打包成apk后我在登录存储的token死都获取不到&#xff0c;导致后续接口请求头没有token连接不到接口&#xff0c;只有运行到手机或者模拟器还有打包成apk后是获取不到&#xff0c;其他的小程序还有网页都可以获取到 试过了很多种方法…

【递归】C++算法:124 二叉树中的最大路径和

作者推荐 【动态规划】【字符串】扰乱字符串 本文涉及的基础知识点 递归 124. 二叉树中的最大路径和 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#x…

Android linphone-android sdk设置语音编码问题

1.遇到的问题 今天遇到linphone-android sdk需要解决语音编码问题&#xff0c;需要指定编码。查了下配置&#xff0c;里面没有发现类似的配置。 ## Start of factory rc # This file shall not contain path referencing package name, in order to be portable when app is r…

基于SpringBoot+thymeleaf的养老院管理系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

rtsp超分推流流程(一)

记录一下rtsp 推流的流程 播放一个RTSP的流需要如下的RTSP请求命令&#xff1a; OPTIONS -> DESCRIBE -> SETUP -> PLAY 然后就是接收RTP包、播放了 抓包分析出来&#xff0c;推流的RTSP请求命令如下&#xff1a; OPTIONS -> ANNOUNCE -> SETUP -> RECORD …

本地引入Element UI后导致图标显示异常

引入方式 npm 安装 推荐使用 npm 的方式安装&#xff0c;它能更好地和 webpack 打包工具配合使用。 npm i element-ui -SCDN 目前可以通过 unpkg.com/element-ui 获取到最新版本的资源&#xff0c;在页面上引入 js 和 css 文件即可开始使用。 <!-- 引入样式 --> <…

blender Texture Coordinate Node

Texture Coordinate Node — Blender Manual Generated纹理坐标是一种自动生成的纹理坐标&#xff0c;它不需要用户指定任何UV映射或对象空间。它是基于对象的边界框的坐标&#xff0c;也就是说&#xff0c;它是根据对象的最小点和最大点来定义的。 对象的边界框是一个包围对…

使用requests发请求操作Elasticsearch【一】

本文为博主原创&#xff0c;未经授权&#xff0c;严禁转载及使用。 本文链接&#xff1a;https://blog.csdn.net/zyooooxie/article/details/123730279 之前在测试环境查es数据&#xff0c;在用 Kibana&#xff1b;可下半年&#xff0c;因为某些原因 就不能用了。我就想着用代…

[NSSRound#3 Team]This1sMysql

[NSSRound#3 Team]This1sMysql 源码 <?php show_source(__FILE__); include("class.php"); $conn new mysqli();if(isset($_POST[config]) && is_array($_POST[config])){foreach($_POST[config] as $key > $val){$value is_numeric($var)?(int)$…

【linux】日志管理和分析

一、概述 在Linux系统的管理和运维中&#xff0c;日志文件起到至关重要的作用。它们记录了系统运行过程中的各种事件&#xff0c;包括系统故障、性能数据和安全事件。 二、 日志的作用和分类 日志的作用 日志文件记载了系统的生命线&#xff0c;利用它们可以&#xff1a; 1…

ubuntu22.04 手动分区说明

主分区和逻辑分区的区别 主分区&#xff1a;主分区是硬盘分区表中的前四个分区&#xff0c;用于安装操作系统、创建引导分区等。主分区可以设置为启动分区和活动分区&#xff0c;是直接在硬盘上划分的。要在硬盘上安装操作系统&#xff0c;则硬盘必须有1个主分区。 逻辑分区&…

震惊!原来这就是JavaScript闭包的秘密

&#x1f4e2; 鸿蒙专栏&#xff1a;想学鸿蒙的&#xff0c;冲 &#x1f4e2; C语言专栏&#xff1a;想学C语言的&#xff0c;冲 &#x1f4e2; VUE专栏&#xff1a;想学VUE的&#xff0c;冲这里 &#x1f4e2; CSS专栏&#xff1a;想学CSS的&#xff0c;冲这里 &#x1f4…

Qt 中如何将图片转化为yuv420p

使用 QImage 存储的图片&#xff0c;如果想转成yuv420p 发送出去&#xff0c;该怎么办呢&#xff1f; QImage 存储图片有多种格式&#xff0c;可以通过image.format() 查看当前的格式&#xff1b; 建议通过将格式转换为mage.convertToFormat(QImage::Format_RGB888)&#xff0c…

硬盘基本知识(磁头、磁道、扇区、柱面)

概述 盘片&#xff08;platter&#xff09; 磁头&#xff08;head&#xff09; 磁道&#xff08;track&#xff09; 扇区&#xff08;sector&#xff09; 柱面&#xff08;cylinder&#xff09; 盘片 片面 和 磁头 硬盘中一般会有多个盘片组成&#xff0c;每个盘片包含两个面…

Eviews 11安装包下载及安装教程

Eviews 11下载链接&#xff1a;https://docs.qq.com/doc/DUmRGdXVUeVBSU1lK 1.选中下载好的安装包&#xff0c;右键解压到“Eviews 11”文件夹 2.选中"Setup.exe"鼠标右键选择以管理员身份运行 3.点击“Next” 4.勾选“I Accept...”&#xff0c;点击“Next” 5.选择…

理解String 及 String.intern() 在实际中的应用

1. 首先String不属于8种基本数据类型&#xff0c;String是一个对象。     因为对象的默认值是null&#xff0c;所以String的默认值也是null&#xff1b;但它又是一种特殊的对象&#xff0c;有其它对象没有的一些特性。   2. new String()和new String(“”)都是申明一个…

记一次 .NET某工控 宇宙射线 导致程序崩溃分析

一&#xff1a;背景 1. 讲故事 为什么要提 宇宙射线, 太阳耀斑 导致的程序崩溃呢&#xff1f;主要是昨天在知乎上看了这篇文章&#xff1a;莫非我遇到了传说中的bug&#xff1f; &#xff0c;由于 rip 中的0x41变成了0x61出现了bit位翻转导致程序崩溃&#xff0c;截图如下&am…

Jenkins接口调用

Jenkins是好用&#xff0c;但是接口文档写的稀烂 1、授权&#xff0c;Jenkins不推荐使用创建单个任务时创建的token&#xff0c;推荐这个用户下的创建user token。 点击自己账号信息&#xff0c;即可创建token。 2、postman选择basic auth&#xff0c;输入账号密码&#xff0…