Socket与TCP的关系

news2024/11/19 3:18:06

前言

相信大家对于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.coloradmin.cn/o/1358844.html

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

相关文章

Softing LinkXpert M3荣获Connect Professional“2023年度产品”

2023年7月21日&#xff0c;Softing IT Networks凭借其LinkXpert M3产品荣获2023年度Connect Professional读者选择的“2023年度产品”奖项。 在信息及通信技术测量类别中&#xff0c;LinkXpert M3从众多竞争者中脱颖而出&#xff0c;最终获得提名并跻身“2023年度产品”之列。该…

Gin 路由注册与请求参数获取

Gin 路由注册与请求参数获取 文章目录 Gin 路由注册与请求参数获取一、Web应用开发的两种模式1.前后端不分离模式2.前后端分离模式 二、RESTful介绍三、API接口3.1 RESTful API设计指南3.2 API与用户的通信协议3.3 RestFul API接口设计规范3.3.1 api接口3.3.2 接口文档&#xf…

使用 PHP-FFMpeg 操作视频/音频文件

做音频合成的时候找到的一个php操作ffmpeg 的类库。GitHub地址&#xff1a;https://github.com/PHP-FFMpeg/PHP-FFMpeg/。本文的例子大部分都是上面的 在使用之前请安装好 FFMpeg 。如何安装&#xff1f;请看 FFmpeg 安装教程。 使用composer快速安装 > composer require …

VUE3跳转页面时 定时器未清除解决

一,问题 1、在vue中使用setTimeout定时器的时候&#xff0c;可能会遇到关不掉的情况&#xff0c;会存在明明已经在beforeDestroy和destroyed中设置了定时器清除了&#xff0c;但是有时候没生效&#xff0c;定时器还会继续执行。 2、在这里需要说一下setTimeout的使用场景&…

【React系列】受控非受控组件

本文来自#React系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. refs 的使用 在React的开发模式中&#xff0c;通常情况下不需要、也不建议直接操作DOM原生&#xff0c;但是某些…

红日靶场第一关stack靶场

安装黑客后台工具&#xff0c;使用cs工具 也就是cobaltstrike工具 首先我们先使用msfvenom工具生成一个用于windows的木马后台文件&#xff0c;然后我们将生成的木马文件名字叫做620.exe 然后我们要使用msfconsole工具监听咱们的木马后门 启动监听 用蚁剑开启我们所设置的木…

商城系统哪家好,如何选择商城系统?

之前我们做电商项目时&#xff0c;通过百度搜索“java商城系统”、”商城系统有哪些“等关键词&#xff0c;发现各类系统琳琅满目&#xff0c;我们先按商城系统推出时间做了一个统计&#xff0c;然后针对各系统进行了一定的调研。 推出时间超过10年的&#xff1a;shop、ecshop…

Java爬虫之Jsoup

1.Jsoup相关概念 Jsoup很多概念和js类似&#xff0c;可参照对比理解 Document &#xff1a;文档对象。每份HTML页面都是一个文档对象&#xff0c;Document 是 jsoup 体系中最顶层的结构。 Element&#xff1a;元素对象。一个 Document 中可以着包含着多个 Element 对象&#…

纯前端上传word,xlsx,ppt,在前端预览并下载成图片(预览效果可以,下载图片效果不太理想)

纯前端上传word,xlsx,ppt,在前端预览并下载成图片&#xff08;预览效果可以&#xff0c;下载图片效果不太理想&#xff09; 一.安装依赖二、主要代码 预览效果链接: https://github.com/501351981/vue-office 插件文档链接: https://501351981.github.io/vue-office/examples/d…

webrtc报文记录

tcp.port 10443 || tcp.port 6080 || udp.port 8000 https://download.csdn.net/download/dualvencsdn/88706745

LeetCode 2397. 被列覆盖的最多行数:二进制枚举

【LetMeFly】2397.被列覆盖的最多行数&#xff1a;二进制枚举 力扣题目链接&#xff1a;https://leetcode.cn/problems/maximum-rows-covered-by-columns/ 给你一个下标从 0 开始的 m x n 二进制矩阵 mat 和一个整数 cols &#xff0c;表示你需要选出的列数。 如果一行中&am…

数据结构和算法-插入排序(算法效率 折半优化 顺序表与链表插入排序 代码实现)

文章目录 插入排序算法实现算法效率分析优化-折半插入排序代码实现对链表进行插入排序小结 插入排序 首先49当作第一个已经排好序得元素&#xff0c;将第二个元素与前面得元素对比&#xff0c;发现小于49&#xff0c;于是49移动位置 此时将65与之前元素对比&#xff0c;发现其…

【C++期末编程题题库】代码+详解18道

适合期末复习c看&#xff0c;或者刚入门c的小白看&#xff0c;有的题会补充知识点&#xff0c;期末复习题的代码一般比较简单&#xff0c;所以语法上没那么严谨。本文所有题目要求全在代码块的最上面。 目录 1、设计复数类 2、设计Computer类 3、实现相加的函数模板 4、圆类…

全网最简单vscode使用Makefile调试多文件的C/C++代码

前言 vscode调试C/C教程很多&#xff0c;操作麻烦&#xff0c;这里试图找到一个最简单的使用vscode调试C/C代码的方法。这里是使用Makefile的多文件方式。 测试文件 tree . ├── func.c ├── func.h ├── main.c └── Makefilefun.c #include <stdio.h> #in…

对低效的会议说“不!”

根据微软对全球 31, 000 名员工开展的一项调查&#xff0c;低效的会议是影响工作效率的第一大干扰因素&#xff0c;其次是召开过多的会议。 大大小小的同步会、讨论会、审查会、复盘会不仅将工作时间拆解得支离破碎&#xff0c;还会让成员因「会议恢复综合症」而无法立即从无效…

Certum与Geotrust的OV多域名证书

Certum和Geotrust都是知名的CA认证机构&#xff0c;旗下的SSL证书产品丰富&#xff0c;有单域名SSL证书、多域名SSL证书以及通配符SSL证书。这些SSL数字证书作为一种重要的网络安全产品&#xff0c;能够实现数据加密和身份验证&#xff0c;保障网站的安全性和隐私性。OV多域名S…

【快速全面掌握 WAMPServer】14.各种组件的升级方法

网管小贾 / sysadm.cc WAMPServer 更新很快&#xff0c;这是件好事&#xff01; 但是 WAMPServer 更新快是因为他很勤劳吗&#xff1f; 其实这个问题的原因并不是出自 WAMPServer 自身&#xff0c;而是来自它的各个组件。 是的&#xff0c;你能想像得到&#xff0c;比如 PHP…

数据结构和算法-数据结构的基本概念和三要素和数据类型和抽象数据类型

文章目录 总览数据结构的基本概念总览数据早期和现代的计算机处理的数据数据元素-描述一个个体数据对象-一类数据元素什么是数据结构小结 数据结构的三要素总览逻辑结构-集合结构逻辑结构-线性结构逻辑结构-树形结构逻辑结构-图形结构逻辑结构-小结数据的运算物理结构&#xff…

Struts2 远程代码执行漏洞S2-001分析

自 Struts2 在 2007 年爆出第一个远程代码执行漏洞 S2-001 以来&#xff0c;在其后续的发展过程中不断爆出更多而且危害更大的远程代码执行漏洞&#xff0c;而造成 Struts2 这么多 RCE 漏洞的主要原因就是 OGNL 表达式。这里以 Struts2 的第一个漏洞 S2-001 为例来对 Struts2 远…

新年启新程 | 开门红!菊风中标重庆三峡银行双录及产品销售可回溯系统项目

INTRODUCTION 近年来&#xff0c;随着人们需求的转变和金融科技的高速发展&#xff0c;银行开始朝着数智化方向转型。为顺应客户行为变迁&#xff0c;银行同业积极构建远程银行云服务生态。同时&#xff0c;面对业务的升级以及新的监管要求&#xff0c;现有音视频功能难以满足…