中科大计网学习记录笔记(十二):TCP 套接字编程
前前言:大家看到这一章节的时候一定不要跳过,虽然标题是编程,但实际上是对 socket 的运行机制做了详细的讨论,对理解 TCP 有很大的帮助;但是由于本节涉及到了大量的编程知识,对于一些朋友来说不是很好理解,所以大家看本节的时候强烈建议结合我的这篇笔记来学习,本篇整理了许多详细的案例和解析,也补充了我自己的理解,导致这篇笔记来到了七千多字,工程量很大,如果这篇文章对你有所帮助别忘了留下你的点赞和关注😊😊😊
前言:
学习视频:中科大郑烇、杨坚全套《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》课程
该视频是B站非常著名的计网学习视频,但相信很多朋友和我一样在听完前面的部分发现信息量过大,有太多无法理解的地方,在我第一次点开的时候也有相同的感受,但经过了一段时间项目的学习,对计网有了更多的了解,所以我准备在这次学习的时候做一些记录并且加入一些我的理解,希望能够帮助到大家。
往期笔记可以看专栏中的内容😊😊😊
文章目录
- 2.8 TCP 套接字编程
- 2.8.1 通过 TCP 建立连接的流程
- 2.8.2 编程案例
- 2.8.3 总结
2.8 TCP 套接字编程
💡 Socket 编程:应用进程使用 传输层 提供的服务才能够交换报文,实现应用协议从而实现引用。
- 在 TCP/ IP 的架构中,应用进程使用 Socket API 来 访问 传输的服务
- 传输层中 TCP 提供的是可靠的 字节流 的服务,UDP 提供的是不可靠的数据 UDP 数据报的服务
- 字节流需要上层自己来维护界限,也就是 TCP 提供字节流,但是需要自己来划清有哪些部分
2.8.1 通过 TCP 建立连接的流程
前期准备: 服务器
-
服务器进程必须先处于运行的状态
-
创建欢迎 socket
-
和本地的 端口 捆绑
-
在原本创建的 socket 上阻塞式等待接收用户的连接
-
-
客户端
- 创建客户端本地的 socket,捆绑到本地的端口
- 与指定的服务器的进程和 IP 地址和端口,发送建立连接的请求
- 然后这个 socket 会在本地等候连接
- 创建客户端本地的 socket,捆绑到本地的端口
-
服务器
-
当服务器接收到用户的请求时,接触阻塞,创建一个新的 socket 值,与客户端通信
-
原初的监听 socket 并不会消失,它继续存在并可以接收更多的客户端连接请求。服务器端的监听 socket 负责监听和接受连接。
-
-
客户端
- 客户端收到信息后,确认收到信息,再次向客户端发送报文表示确认链接
💡 上面的过程其实就是简化过的 TCP 三次握手 的过程
- 上述过程中的端口中,服务器的端口是指定的,就是对外提供服务的端口(每个服务器都有对外提供访问服务的端口)
- 而客户端的端口可能是随机指定的,比如编写一段 Java 程序跑在
8080
端口,程序向数据库服务器发送请求(作为客户端),数据库服务器提供的访问端口为3306
,返回的数据不会直接发送到Java程序监听的8080
端口,而是发回到客户端在连接时使用的随机源端口上。
- 这个例子是为了让大家更好的理解服务器的客户端,因为在命令执行的时候服务器其实就是发送请求的客户端,而 同时 又作为对外提供服务的服务器。
2.8.2 编程案例
💡 这里使用一个案例来更好的描述 socket 的运转流程
- 客户端接收一个仅包含小写字母的输入
- 将输入部分发送给服务器
- 服务器将其转换为大写并且返回
👉 在开始之前先来介绍两个结构体(存储信息的一种数据结构)
🍀 sockaddr_in
:是 C 语言编程中用于表示 Internet (IPv4) 地址的结构体,通常在 BSD sockets API 中使用。
struct sockaddr_in {sa_family_t sin_family; // 地址族,对于 IPv4 应该为 AF_INET(通常定义为2)in_port_t sin_port; // 端口号(网络字节序,即大端字节序)struct in_addr sin_addr; // IPv4 地址结构体unsigned char sin_zero[8]; // 填充字段
};
💡 概念辨析
- 地址族:地址族用来定义不同类型的网络层协议以及这些协议如何识别并处理网络地址。每一种地址族对应一种特定的网络协议或一系列相关的协议,支持相应的地址格式和相关功能
- 比如
AF_INET
代表 IPv4 地址族- 填充字段:对于不同的网络协议(IPv4、IPv6 等)是将其对应的结构转换为一个通用的结构来进行信息的传递的,所以能够将其转换为这个统一的结构且不会出现匹配问题,就需要一个填充字段来使得更具备兼容性。
再来看这个结构体的存储信息的意义
- 这个结构体在通信过程中表示的是通信的 一方,当客户端或服务器需要指定一个网络地址进行连接、绑定或接收连接时,就会使用
sockaddr_in
结构体 - 这个端点既可以是本地端(例如服务器绑定到的IP地址和端口),也可以是远程端(例如客户端要连接的目标服务器的IP地址和端口)。
🍀 hostent
:在C语言的网络编程中用于存储与主机名解析相关的信息,它包含了从域名系统(DNS)获取到的一个特定主机的所有相关信息。
struct hostent {char *h_name; // 主机的正式名字(规范化主机名)char **h_aliases; // 指向别名列表的指针数组,int h_addrtype; // 地址类型,例如AF_INET(IPv4)、AF_INET6(IPv6)int h_length; // 地址长度,以字节为单位char **h_addr_list; // 指向主机IP地址列表的指针数组
};
💡 这一部分是结合完整的 C 语言代码的演示,不用去看懂代码,熟悉完整的流程即可
🍀 首先,服务器启动程序,并且创建一个 欢迎 socket 来等待客户端的请求:
第一步:变量声明
int server_sock, client_sock;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);char buffer[BUFFER_SIZE];
💡 这里先不对每个变量的意义做出解释,后面在流程中一一阐述
第二步:创建 socket
// 创建 socket if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}
💡 解析
server_sock
,在之前的 socket 部分提到过,上层应用访问 socket 是通过一个整型,这个整型就标识着这个 socket。而调用函数创建一个 socket 返回的就是这个标识。- 上述的创建过程包括很多后续的创建过程都是通过
if
创建的,旨在如果创建失败的话会执行if
中的语句来做失败处理,是一种常见的方式。
第三步:设置服务器地址结构体(也就是上面提到的 sockaddr_in
)
// 设置服务器地址结构体memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8000); // 假设监听8000端口server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有网络接口
💡 AF_INET 就是上面提到的 IPv4 协议族
第四步:绑定 socket 到指定的端口
// 绑定 socket 到指定端口if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");exit(EXIT_FAILURE);}
💡 这一步就是将创建的 socket 实实在在的绑定到本机的端口上了
- 在本步骤之前,socket 都是一个系统返回的整型,在本步骤之后它就与本地的端口做了绑定
- 上面的 sockaddr 就是上面提到的统一的数据结构,绑定等操作不会直接对
server_addr
或者client_addr
做操作,而是将其转换为sockaddr
结构。
第五步:开始监听请求
// 开始监听连接请求if (listen(server_sock, 10) == -1) { // 最多允许10个未处理的连接请求排队perror("listen failed");exit(EXIT_FAILURE);}
💡 创建完上面的 socket,也就是常说的 HelloSocket
- 并且将请求放于队列中等待,这个队列最多存储十个请求,当请求多于十个的时候会拒绝请求。
- 通过这个命令将 socket 置于监听的状态
🍀 接下来将视角转向客户端
第一步:变量声明
int client_sock;struct sockaddr_in server_addr;char buffer[BUFFER_SIZE];char input[BUFFER_SIZE];
第二步:创建 socket
// 创建 socketif ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}
💡 这个 socket 是标识本地的 socket
- 与服务器不同的是服务器需要额外的去指定自己的端口(不能随便分配,因为这个端口是要告知客户端来使其访问的)
- 所以对于客户端的 socket 来说,不需要去手动指定端口,而是在调用
connect()
函数与服务器建立连接时,操作系统会 自动 为该socket分配一个可用的临时端口作为源端口(这个端口号通常是随机选择的,范围是1024至65535)。
第三步:设置服务器地址的结构体
// 设置服务器地址结构体memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("invalid address/ Address not supported");exit(EXIT_FAILURE);}
💡 服务器的信息是提前告知用户的
- 中间可能会通过
hostent
来通过域名解析得到IP
地址- 本例子中没有涉及
第四步:与服务器建立连接
// 连接到服务器
if (connect(client_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("connect failed");exit(EXIT_FAILURE);
}
💡 需要传入本地的
socket
和服务器的信息
🍀 继续回到服务器
服务器本地等待连接
while (1) {
// 接受客户端连接请求if ((client_sock = accept(server_sock, (struct sockaddr *)&client_addr ,&client_len)) < 0) {perror("accept failed");continue;}// 其他代码
}
💡 这段代码之后服务器的代码都是写在这个 while 函数中的
accept
函数会阻塞的等待连接,当 listen 队列中有连接的时候会去除并且处理
🍀 回到客户端来发送信息
发送数据到服务器
// 发送数据到服务器if (send(client_sock, input, strlen(input), 0) < 0) {perror("send failed");exit(EXIT_FAILURE);}
💡 当建立连接后客户端就可以发送信息了
- 这时候本地的 socket 就与服务器连接上了,通过这个 socket 就可以实现与服务器的通信了
🍀 服务器来处理客户端的请求
// 接收客户端发送的数据ssize_t bytes_received = recv(client_sock, buffer, BUFFER_SIZE - 1, 0);if (bytes_received <= 0) {perror("recv failed");close(client_sock);continue;}buffer[bytes_received] = '\0'; // 确保字符串结束// 转换小写为大写to_upper(buffer);// 发送数据回客户端if (send(client_sock, buffer, strlen(buffer), 0) < 0) {perror("send failed");}// 关闭已处理的客户端连接close(client_sock);
💡 上述的代码是写在 while 循环中的,表示对一个连接的处理
- 接下来将信息发送回客户端
- 注意这时候已经是通过
client_socket
来发送数据了,这个是在上面通过accept()
函数来返回的 新 的socket
,表示一个连接- 原本的
Hello Socket
仍然在listen
状态等候连接
🍀 最后就是客户端接收返回的信息
客户端接收信息
// 接收服务器发来的数据ssize_t bytes_received = recv(client_sock, buffer, BUFFER_SIZE - 1, 0);if (bytes_received <= 0) {perror("recv failed");} else {buffer[bytes_received] = '\0';printf("Received uppercase word from server: %s\n", buffer);}// 关闭客户端socketclose(client_sock);
2.8.3 总结
💡 相信看到这里大家对 socket 的执行流程已经有了大致的把握,这里换个视角做一个总结。
其实整个流程就是 不断调用 socket API 来实现各种服务,这体现了下层为上层通过接口提供服务的特点。
这里来回顾一下上面代码中使用到的 socket API
- socket():函数用于创建一个新的socket,参数指定了地址族(AF_INET表示IPv4)、套接字类型(SOCK_STREAM表示TCP流式套接字)和协议(通常为0,由系统选择合适的协议)
- 用于上面 socket 的创建,比如说客户端和服务器的 socket
- 当这些信息都不指定的时候返回的就是一个普通的整型,没有任何绑定
- 这个绑定可以通过后续的 bind 和 connect 来额外附加,最终使用的 socket 一定是绑定了客户端和服务器的全部信息的
- 所以后续的一部分操作就是针对这个 socket 信息的修补,来让它达到可以使用的状态
- bind():绑定一个本地IP地址和端口到服务器socket。
server_addr
是一个初始化好的sockaddr_in
结构体,包含了服务器要监听的IP地址和端口号信息。- 这就是服务器修补 socket 的方式,将自己的端口号等信息补充到里面
- listen():将服务器socket设置为监听模式,并允许指定数量(在例子中是10个)的未完成连接请求排队等待。
- accept():该函数使服务器socket进入阻塞状态,直到有新的客户端连接请求到达。成功建立连接后,返回一个新的socket描述符
client_sock
,用来与新客户端通信,并通过client_addr
获取客户端的地址信息。- 这里的 socket 就是一个完完整整可以使用的 socket,通过 accept 函数补充上了客户端的信息
- connect():通常用于客户端程序连接到服务器端的 socket
- 这是客户端用来修补自己 socket 的方式,调用后操作系统会自动分配临时端口信息
- send() 和 recv():
recv()
用于从已连接的socket接收数据,而send()
则用于向已连接的socket发送数据。 - close():关闭已打开的socket文件描述符,释放系统资源。
这就引出了另一种理解方式:在真正的业务开始之前,所有的操作都是为了使得通信双方获得 完整的 socket
- 客户端有了服务器的信息所以可以直接获取完整的 socket
- 而服务器需要等待客户端发送信息给它才能获取完整的 socket,所以服务器的 socket 会有 listen 的状态来等待信息补充完整。
💡 补充:三次握手
- 三次握手是TCP(Transmission Control Protocol)建立连接的过程,确保了客户端和服务器之间的数据传输通道可靠、有序且无差错。
- 第一次握手:
- 客户端发送一个SYN(同步序列编号,Synchronize Sequence Numbers)报文段到服务器,其中包含客户端选择的一个初始序号seq=x。此时,客户端进入SYN_SENT状态。
- 第二次握手:
- 服务器接收到客户端的SYN报文段后,如果同意建立连接,则回应一个SYN+ACK(同步并确认)报文段。这个报文段中包含了服务器的初始序号seq=y,并将确认号ack设置为x+1,表示已接收并期望接下来的数据从x+1开始。服务器进入SYN_RECEIVED状态。
- 第三次握手:
- 客户端收到服务器的SYN+ACK报文段后,向服务器发送一个ACK(确认)报文段。该报文中确认号ack设置为y+1,表示已经接收到了服务器的SYN,并且期望接下来的数据从y+1开始。客户端同时也将自己的序号seq更新为x+1。
以上面提到的视角看,其实三次握手就是补充 socket 信息的一个过程
相关文章:

中科大计网学习记录笔记(十二):TCP 套接字编程
前前言:大家看到这一章节的时候一定不要跳过,虽然标题是编程,但实际上是对 socket 的运行机制做了详细的讨论,对理解 TCP 有很大的帮助;但是由于本节涉及到了大量的编程知识,对于一些朋友来说不是很好理解&…...

落实三大阶段目标,TRON全方位打通与BTC生态互联
2月15日,波场TRON创始人、火币HTX全球顾问委员会委员孙宇晨在X平台发布公告表示,波场TRON已正式公布比特币第二层解决方案及路线图,围绕打通比特币与波场TRON网络的跨链连接、投资开发用户友好的钱包和工具,同时与多个比特币第二层协议进行合作等重点,全方位拥抱比特币发展机遇…...

MCU中断控制
目录 一、中断相关基础知识 1、NVIC:嵌套向量中断控制器 2、可屏蔽中断和不可屏蔽中断的区别 3、中断优先级 4、常见特殊中断 二、中断相关寄存器 三、中断使用步骤: 一、中断相关基础知识 1、NVIC:嵌套向量中断控制器 (1) 它是内核的…...

C语言中的可变参数
目录 可变参数函数原理与分析总结 实现方案1、 va_start 宏2、 va_arg 宏3、 va_end 宏 应用举例举例1:提前已知所有参数类型的简单情况举例2:通过固定参数,来动态确定可变参数类型的复杂情况 可变参数函数 在C语言中,有这样的一…...

Leetcode-103. 二叉树的锯齿形层序遍历
这个年和树过不去啦啦啦! 题目: 给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 示例 1&…...

vs code“无法与远程服务器建立连接:XHR failed.”解决办法
获取到 commit id 的方式参考: vscode通过ssh链接服务器卡在downloading with wget - 知乎 关于下载 vscode-server-linux-x64.tar.gz,浏览器打开: https://vscode.download.prss.microsoft.com/dbazure/download/stable/你的commit id/vs…...

第五节 zookeeper集群与分布式锁_2
1.分布式锁概述 1.1 什么是分布式锁 1)要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁。 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。 线程锁只在同一J…...

Shell脚本——提取目录名和文件名
目录 一、${} 1.${var##*/} 2.${var##*.} 3.${var#*.} 4.${var%/*} 5.${var%%.*} 6.总结 二、basename和dirname 1.basename 2.dirname 在许多场景下,我们都需要对文件名称或者文件所在的目录进行操作,已达到我们业务目的。通常的操作是由路径…...

wps使用方法(包括:插入倒三角符号,字母上面加横线,将word中的所有英文设置为time new roman)
倒三角符号 字母上面加横线 将word中的所有英文设置为time new roman ctrla选中全文...

备战蓝桥杯---图论之最小生成树
首先,什么是最小生成树? 他就是无向图G中的所有生成树中树枝权值总和最小的。 如何求? 我们不妨采用以下的贪心策略: Prim算法(复杂度:(nm)logm): 我们对于把上述的点看成两个集…...

爬虫-华为云空间备忘录导出到docx-selenium控制浏览器行为-python数据处理
背景适用情况介绍 老的荣耀手机属于华为云系统,家里人换了新荣耀手机属于荣耀云系统无法通过云空间将备忘录转移到新手机,不想让他们一个一个搞,于是整了一晚上想办法爬取下来。从网页抓取下来,然后存到docx文档中(包…...

网络安全的新防线:主动进攻,预防为先
进攻性安全(Offensive security)是指一系列主动安全策略,这些策略与恶意行为者在现实世界的攻击中使用的策略相同,区别在于其目的是加强而非损害网络安全。常见的进攻性安全方法包括红队、渗透测试和漏洞评估。 进攻性安全行动通常…...

基于java springboot+mybatis学生学科竞赛管理管理系统设计和实现
基于java springbootmybatis学生学科竞赛管理管理系统设计和实现 🍅 作者主页 央顺技术团队 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 文末获取源码联系方式 📝 🍅 查看下方微信号获取联系方式 承接各…...

秒懂百科,C++如此简单丨第二十一天:栈和队列
目录 前言 Everyday English 栈(Stack) 图文解释 实现添加删除元素 实现查看清空栈 完整代码 运行示例 栈的选择题 队列(Queue) 图文解释 队列的基本用法 完整代码 运行结果 队列的好处 结尾 前言 今天我们将…...

STM32-开发环境之STM32CubeMX
目录 STM32CubeMX介绍 STM32CubeMX特性 应用场景 其他事项 STM32CubeMX介绍 STM32CubeMX是ST公司(意法半导体)推出的一款图形化工具,也是配置和初始化C代码生成器。它主要服务于STM32微控制器的配置和开发。 STM32CubeMX特性 1.直观选…...

[晓理紫]CCF系列会议截稿时间订阅
CCF系列会议截稿时间订阅 VX 关注{晓理紫},每日更新最新CCF系列会议信息,如感兴趣,请转发给有需要的同学,谢谢支持!! 如果你感觉对你有所帮助,请关注我,每日准时为你推送最新CCF会议…...

重复导航到当前位置引起的。Vue Router 提供了一种机制,阻止重复导航到相同的路由路径。
代码: <!-- 侧边栏 --><el-col :span"12" :style"{ width: 200px }"><el-menu default-active"first" class"el-menu-vertical-demo" select"handleMenuSelect"><el-menu-item index"…...

如何在 Angular 中使用 Flex 布局
介绍 Flex Layout 是一个组件引擎,允许您使用 CSS Flexbox 创建页面布局,并提供一组指令供您在模板中使用。 该库是用纯 TypeScript 编写的,因此不需要外部样式表。它还提供了一种在不同断点上指定不同指令以创建响应式布局的方法。 在本教…...

通俗的讲解什么是机器学习之损失函数
想象一下,你在玩一个靶心射击的游戏,你的目标是尽可能让箭簇命中靶心。在这个游戏中,损失函数可以看作是测量你的箭簇与靶心距离的规则。损失函数的值越小,意味着你的箭簇离靶心越近,你的射击技能越好。 在机器学习中…...

快速搭建PyTorch环境:Miniconda一步到位
快速搭建PyTorch环境:Miniconda一步到位 🌵文章目录🌵 🌳一、为何选择Miniconda搭建PyTorch环境?🌳🌳二、Miniconda安装指南:轻松上手🌳🌳三、PyTorch与Minic…...

图灵日记之java奇妙历险记--抽象类和接口
目录 抽象类概念抽象类语法 接口概念规则使用特性实现多个接口接口的继承接口使用实例Clonable接口和深拷贝抽象类和接口的区别 Object类 抽象类 概念 在面向对象的概念中,所有对象都是通过类来描述的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够…...

批量给元素添加进场动画;获取文本光标位置;项目国际化
批量给元素添加进场动画 api及参数参考:https://juejin.cn/post/7310977323484971071 简单实现: addAnimationClass(){//交叉观察器if (window?.IntersectionObserver) {//获取所有需要添加进场动画的元素,放到一个数组let items [...do…...

解决:docker创建Redis容器成功,但无法启动Redis容器、也无报错提示
解决:docker创建Redis容器成功,但无法启动Redis容器、也无报错提示 一问题描述:1.docker若是直接简单使用run命令,但不挂载容器数据卷等参数,则可以启动Redis容器2.docker复杂使用run命令,使用指定redis.co…...

Jlink+OpenOCD+STM32 Vscode 下载和调试环境搭建
对于 Mingw 的安装比较困难,国内的网无法正常在线下载组件, 需要手动下载 x86_64-8.1.0-release-posix-seh-rt_v6-rev0.7z 版本的软件包,添加环境变量,并将 mingw32-make.exe 名字改成 make.exe。 对于 OpenOCD,需要…...

单片机在物联网中的应用
单片机,这个小巧的电子设备,可能听起来有点技术性,但它实际上是物联网世界中的一个超级英雄。简单来说,单片机就像是各种智能设备的大脑,它能让设备“思考”和“行动”。由于其体积小、成本低、功耗低、易于编程等特点…...

16.Qt 工具栏生成
目录 前言: 技能: 内容: 1. 界面添加 2. 信号槽 功能实现 参考: 前言: 基于QMainWindow,生成菜单下面的工具栏,可以当作菜单功能的快捷键,也可以完成新的功能 直接在UI文件中…...

【Linux内核】从0开始入门Linux Kernel源码
🌈 博客个人主页:Chris在Coding 🎥 本文所属专栏:[Linux内核] ❤️ 前置学习专栏:[Linux学习]从0到1 ⏰ 我们仍在旅途 目录 …...

SQL Service 2008 的安装与配置
点击添加当前用户...

Apache POI | Java操作Excel文件
目录 1、介绍 2、代码示例 2.1、将数据写入Excel文件 2.2、读取Excel文件中的数据 🍃作者介绍:双非本科大三网络工程专业在读,阿里云专家博主,专注于Java领域学习,擅长web应用开发、数据结构和算法,初步…...

vue 学习definproperty方法
definproperty方法是Vue很重要的一个底层方法,掌握他的原理很重要,下面通过代码说明问题: <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>回顾Object.defineproperty方法</title&…...