课程内容#
套接字是什么?网络编程是做什么的?
- 了解 TCP/IP 五层模型、OSI 七层模型
- 类比
- 套接字 —— 快递员
- 运输层 —— 快递公司:TCP—— 某丰快递公司,UDP—— 某通快递公司
- 交通运输道路 —— 因特网
- 通讯地址 ——IP
—— 运输层协议 ——#
类比快递公司
对于开发者,只能选择 TCP 或 UDP 协议,修改协议参数
TCP#
传输控制协议;面向连接,可靠的数据传输协议
- 连接:三次握手 [详见附加知识点]
- 可靠的本质:确认与重传 [需要有序号]
- 如果丢了就会重传
- [PS] 双方都保存一些描述双方状态的变量
- 头部格式
- 源端口号:从哪个端口发出;目标端口号:发送到哪个端口
- 不同的端口对应不同的应用
- 如果把计算机比作一个大楼,端口号就是大楼里的房间号
- IP 地址由 IP 层给出
- 序列号:标志第几次通信;确认应答号:期望对方下次通信的序列号
- 首部长度:单位为字 [一般为 4 字节]
- 功能比特位[关注标黄处]
- ACK:确认
- RST:重置连接 [拒绝下次连接]
- SYN:建立请求 [三次握手中前两次握手会用到]
- FIN:关闭连接 [四次挥手的第一、第三次挥手会用到,同时还可以放一些数据,详见附加知识点]
- 窗口大小:告诉对方还可以发多少数据,用来抑制对方的发送速率
- 校验和:确认数据是否正确。如果有问题,直接销毁,然后请求重发
- [PS]
- 设计了这么多主要为了可靠
- 现实中的快递公司无法达到可靠,因为运输的物品是唯一的
UDP#
用户数据报协议;无连接,不可靠的数据传输协议
- 无连接:不需要握手
- 不可靠:不管对方是否收到
- 优势:灵活、成本低
- 头部格式
- 相对 TCP,简单得多
——socket——#
类比快递员,但只为一个任务服务
进程和运输层之间的接口,进程发送网络数据必须经它交给运输层去交付
【生与死】#
socket:创建套接字#
- domain:域名类型
- AF_INET,对应 ipv4 [常用]
- AF_INET6,对应 ipv6
- type:类型
- SOCK_STREAM,对应字节流 [TCP]
- SOCK_DGRAM,对应数据报 [UDP]
- protocol:协议
- domain 和 type 可能唯一确定 protocol,如 AF_INET 与 SOCK_STREAM 确定 IPPROTO_TCP
- [PS] 只能选一个的话,可用0代替
- 返回值:文件描述符
- 出错则返回 - 1
- socket 也是一个文件,一切皆文件
close:关闭连接#
- int close(int fd);
- 四次挥手 [详见附加知识点]
- 两端都需要调用 close,调用方发送 FIN,接收端的 recv 的返回值为 0
【服】#
bind:绑定 IP 和端口#
只针对于收数据方
- sockfd:文件描述符
- addr:IP 地址和端口
- 绑定 IP:可以接收来自该 IP 地址的数据 [本机]
- 若为空,则可以接收来自任意 IP 地址的数据
- 可用在内网和外网的交接处,作为防火墙使用
- 绑定端口:服务于哪个端口 [共 $2^{16}=65536$ 个端口]
- 绑定 IP:可以接收来自该 IP 地址的数据 [本机]
- addrlen:地址长度
- 返回值:成功,0;否则,-1
+ 相关结构体:sockaddr、sockaddr_in#
sockaddr
- sin_family:地址协议族,一般使用 AF_INET,对应 ipv4
- sa_data:同时包含 IP 地址和端口
- ❗ 使用并不方便,转用下列更友好的方式👇,再使用 (struct sockaddr*) 强转即可
sockaddr_in
- sin_port:端口号 [需要网络字节序,见下]
- sin_addr:IP地址
- 其中,sin_addr 对应一个新的结构体in_addr
- 存储 32 位无符号整型,一般使用inet_addr函数将点分十进制转换为 in_addr 结构体:
- 点分十进制表示 [字符串形式] 更方便
- inet_ntoa,则反之
+ 主机字节序 & 网络字节序#
- 主机字节序:大端、小端
- 常见为小端机,低位字节排放在内存的低地址端
- 网络字节序:对于 4 个字节的 32bit 值,先传输 0~7bit,...,最后传输 24~31bit
- 整形字节序的转换函数
- htonl:32 位主机字节序到网络字节序的转换
- htons:16 位主机字节序到网络字节序的转换
- ntohl、ntohs,则反之
litsen:设为监听态#
将套接字从主动(默认)切换为被动 [首先需要 bind 绑定端口]
- 注意:第二个参数的真实含义是完成队列的长度
- ① TCP 连接过程存在两个队列
- 未完成队列:客户端发送 SYN 过来,服务器回应 SYN+ACK 之后,服务器当前处于 SYN_RECV 状态,此时的连接处在未完成队列中
- 完成队列:客户端回应 ACK 之后,两边都处于 ESTABLISHED 状态,此时连接从未完成队列转移到完成队列中
- 👉 当服务器调用 accept 时,才将连接从完成队列中移除
- ② 注意事项:设置合适的 backlog;服务端要尽快 accept 新的连接
- ① TCP 连接过程存在两个队列
accept:接受连接#
生成一个新的快递员 [还可继续建立多个连接]
- ① 传入的 sockfd 必须经过 socket ()、bind ()、listen () 处理
- ② addr 为传出参数,用来存储客户端地址
- 返回值
- 成功,则返回一个新的 sockfd,原先的 sockfd 仍可用来 accept
- 失败,则返回 - 1
- [PS] 一般新的 scokfd 使用完毕就将其关闭;listen 状态的 socket 不关闭
【客】#
connect:建立连接#
主动套接字,最多只能连一个
- 与 accept 不同的是:
- sockfd 不需要经过 bind ()、listen () 处理
- 不会返回新的 socket
⭐ connect 和 accept 是一对,分别在客户端和服务端执行,在此期间,完成了三次握手
【传输】#
send:发送数据#
本质同write
- ❗ sendto 多传入了 dest_addr 和 addrlen,它是用于 UDP 的
- 因为没有建立连接,从而需要指定目的 IP 和端口
- flag 一般置为 0
recv:接收数据#
本质同read
- 当对方断开时,返回值为 0
- ❗ recvfrom 多传入了 src_addr 和 addrlen,它是用于 UDP 的
- src_addr 存储发送数据端的地址信息
- 默认是阻塞的
—— 附加 ——#
kill#
给一个进程发送信号
- man 2 kill
- 原型
- 基于进程 ID 和信号位掩码
- 描述
- 设置 pid 有各种形式
- 均需要存在和权限检查
- 返回值
- 0,成功;-1,出错
- kill -l 查看信号列表
- 64 种信号
signal#
信号的处理方式
- man signal
- 原型
- 需要定义一个 sighandler_t 类型的函数
- 描述
- 其行为会随 UNIX 版本变化
- handler 有三种类型:忽略、默认、自定义
- 自定义类型涉及涉及捕鼠器原理:夹住一个老鼠的时候,后面一个老鼠可能被丢失
- 需要重新设置 [由系统操作]
- 返回值
- 根据 handler 而定
代码演示#
服务端#
tcp_server.h
- 在指定端口上创建一个处于监听状态的快递员
tcp_server.c
- 参照序号阅读
- 注意:head.h 中添加 socket 相关的头文件,可在 man 手册中查找,这里不赘述
1.server.c
- accept 可以获取客户端的地址信息
- 创建子进程单独用于传输数据
- 每一步都要注意有错误检测
- + 断开连接情况(FIN,recv 返回为 0)的处理
- 收发策略不同
- 有多少发多少,能收多少收多少
- send 使用 strlen,recv 使用 sizeof
客户端#
tcp_client.h
- 主动连接指定 IP [点分十进制的 ipv4 字符串] 和端口
tcp_client.c
- 填表基于输入
1.client.c
- 加入了对信号的捕捉
- bzero 的使用,初始化 buff 变量
效果展示#
- 左:服务端,右:客户端 [可多用户]
- 建立连接、地址捕获、数据传输、断开连接
- 使用 netstat 可查看端口的监听状态
- 添加 - alnt 选项
- [PS] 需要在云主机的控制台 —— 安全组中开放端口 8888
附加知识点#
- IP:公共的地址服务,尽力而为交付服务。另一层含义,其不可靠 [有可能出车祸]
三次握手、四次挥手#
- 三次握手 [SYN、ACK]
- 第一次握手:客户端发送 SYN 包到服务器 [客户端进入 SYN_SEND 状态,等待服务器确认]
- 第二次握手:服务器收到,必须确认客户端,设置一个 ACK,同时自己也设置一个 SYN,即 SYN+ACK 包 [服务端从 LISTEN 进入 SYN_RECV 状态]
- 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送 ACK 确认包,发送完毕后,客户端进入 ESTABLISHED 状态,服务器收到 ACK 后也进入 ESTABLISHED 状态
- 注意:每次的 ACK 序号,在需要确认的包的序号上加一,表示确认
- 四次挥手 [FIN、ACK]
- 第一次挥手:假设客户端想要关闭连接,客户端发送一个 FIN 包,表示自己已经没有数据可以发送了 [此时仍然可以接收数据][客户端进入 FIN_WAIT_1 状态]
- 第二次挥手:服务端回复一个 ACK 包,表明自己接收到了客户端关闭连接的请求,但自己还需要做些准备来关闭连接 [服务端进入 CLOSE_WAIT 状态]
- 客户端接收到这个 ACK 后,进入 FIN_WAIT_2 状态,等待服务端关闭连接
- 第三次挥手:服务端准备好关闭连接时,向客户端再发送 FIN [服务端进入 LAST_ACK 状态,等待客户端的确认]
- 第四次挥手:客户端接收到来自服务器端的关闭请求,发送一个 ACK 包 [客户端进入 TIME_WAIT 状态,为可能出现的超时重传的 FIN 包,等待2 个 MSL时间]
- 服务端接收到这个 ACK 之后,关闭连接,进入 CLOSED 状态
- 客户端等待了2 个 MSL后,如果没有收到服务端的 FIN,则认为服务端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态;否则,再次发送 ACK
- 参考三次握手与四次挥手—— 博客 [注:第四次挥手客户端等待的是超时重传的 FIN 而不是 ACK]
附加:2 个 MSL 的含义#
TIME_WAIT 是如何引起的,有什么作用,在编程时有什么弊端,怎么解决?
- 引起原因:TCP 的四次挥手时,已经完成前三次挥手,在第四次挥手时,客户端收到来自服务端的 FIN,它在发送一个 ACK 后,就会进入 TIME_WAIT 状态
- 此时客户端需要等待两个最大数据段生命周期(Maximum segment lifetime,MSL)的时间之后,才会进入 CLOSED 状态
- 存在原因
- ①阻止延迟数据段
- 每一个 TCP 数据段都包含唯一的序列号,这个序列号能够保证 TCP 协议的可靠
- 为了保证新 TCP 连接的数据段不会与还在网络中传输的历史连接的数据段重复,TCP 连接在分配新的序列号之前需要至少静默数据段在网络中能够存活的最长时间,即 MSL
- 从而防止延迟的数据段被其他使用相同源地址、源端口、目的地址以及目的端口的 TCP 连接收到
- ②保证连接关闭
- 如果客户端等待的时间不够长,当服务端还没有收到 ACK 消息,而客户端重新与服务端建立 TCP 连接时,会发生:
- 服务端因为没有收到 ACK 消息,所以仍然认为当前连接是合法的
- 客户端重新发送 SYN 消息请求握手时,会收到服务端的 RST 消息,连接建立的过程被终止
- 所以要保证 TCP 连接的远程被正确关闭,即等待被动关闭连接的一方收到 FIN 对应的 ACK 消息
- 如果客户端等待的时间不够长,当服务端还没有收到 ACK 消息,而客户端重新与服务端建立 TCP 连接时,会发生:
- ①阻止延迟数据段
- 编程影响
- 对于高并发的场景容易出现过多的 TIME_WAIT
- 而 MSL 的时长一般是 60s,这是难以接受的,可能一个 TCP 连接只为了通信几秒钟,但 TIME_WAIT 就需要等待 2 分钟
- 解决方式
- 基于一个时间戳变量,记录发送数据包、最近一次接收数据包的时间
- 然后配合两个参数
- reuse:允许主动关闭连接的一方,再次向对方发起连接的时候,复用处于 TIME_WAIT 状态的连接
- recycle:内核会快速回收处于 TIME_WAIT 的连接,只需等待 RTO 时间 [数据包重传的超时时间]
- 参考
C 语言下的 socket 编程#
- 服务端:socket、sockaddr [_in]、bind、listen;accept、send/recv;close
- 客户端:socket、sockaddr [_in]、connect;send/recv;close
- 基于 TCP 的流式套接字、基于 UDP 的数据报套接字
- UDP 服务端也需要 bind IP 与端口,但不需要 listen,使用 sendto、recvfrom 来发、收信息
- sockaddr [_in]:保存 socket 信息的结构体,使用 [_in] 填写信息,再转换为 sockaddr
- 服务端需要两个套接字,一个用来监听,一个用来接收客户端 connect 发送的套接字
输入 kaikeba.com 并按下回车#
-> 到 TCP 建立连接,本地发送第一个 request 报文 -> 到收到第一个 request 报文为止,发生了哪些事情?
- [宏观层面] DNS👉TCP 连接 [应用层、传输层、网络层、数据链路层]👉服务器处理请求👉返回响应结果
- DNS
- 本地 hosts、本地 DNS 解析器缓存
- 本地 DNS
- 迭代 / 递归:根 DNS 服务器,顶级 DNS,权威 DNS
- 直到找到域名对应的 IP
- TCP 连接
- 应用层:发送 HTTP 请求 —— 请求方法、URL、HTTP 版本
- 传输层:与服务器进行三次握手
- 网路层:ARP 协议查询 IP 对应的 MAC 地址,如果在一个局域网内,就直接根据 MAC 地址发送请求;否则使用路由表,查找下一个跳转的地址,再访问对应的 MAC 地址
- 数据链路层:以太网协议
- 广播:向一个局域网中的所有机器发送请求,比较 MAC 地址
- Web 服务器
- 解析用户请求,知道了需要调度哪些资源文件,并调用数据库信息,返回给浏览器客户端
- 返回响应结果
- 一般会有一个 HTTP 状态码,比如 200、301、404 等,通过这个状态码我们可以直到服务器端的处理是否正常,并能了解具体的错误
- ⭐ 推荐视频:TCP-IP Explained (2000)——Youtube
- [主要从 IP 层展开]
- 涉及对象:TCP 包、ICMP Ping 包、UDP 包、死亡之 Ping、路由器、路由器交换机...
- 大致流程
- 本地:封装包、本地传输、本地路由器选择、交换机选择、代理检查、防火墙检查、本地传输、路由器选择
- ——> 网络传输 ——>
- 响应端:防火墙检查 [监管端口]、代理检查请求包、返回相应信息给请求端、同上述本地过程 [封装包、...、路由器选择]
端口复用相关#
一个端口可以同时绑定不同的服务吗?
- 可以。接收数据时,根据五元组 {传输协议,源 IP,源端口,目的 IP,目的端口} 判断数据属性
- 例如:
- 使用 TCP 和 UDP 传输协议监听同一个端口后,接收数据互不影响,不冲突
- 同样,accept 产生新的 socket,还是用的同一个端口
- 产生了多个不同的 socket,这些 socket 里包含的目的 IP 和端口是不变的,变化的只是源 IP 和端口 [端口复用]
- [PS] TCP 类型 socket 只给 TCP 类型发数据
父子进程的 socket 关系#
父进程克隆出的子进程里的 socket 和父进程的 socket 的关系
- 是同一个,对应同一个文件
- 当有数据到来时,两个进程谁先收到数据则谁有该数据,另一个进程继续等待
- 所以一般地,子进程不需要的资源就不要继承,如:可使用 close 直接关闭子进程中继承自父进程的 socket
Tips#
- 系统 / 网络编程要考虑所有可能出错的地方
- 信号知识扩展:实现自己的 sleep 函数
- 编译时要记得考虑所有相关的源文件 [*.c]