
目录
- 1、初识协议
- 2、Mac、IP地址
- 3、端口号
- 4、网络字节序
- 5、socket
1、初识协议
- 协议就是一种约定。
- 如何让不同厂商生产的计算机之间能够互相通信?需要由权威组织或公司制定网络协议。
- 协议本质也是软件,在设计上为了更好的进行模块化,解耦合,因此被设计为层状结构。
协议本质也是软件,为了更好的模块换,降低耦合度,所以被设计为层状结构。在Linux网络协议栈中,各个层次协同工作,以实现数据的封装、传输、路由和接收。从底层到高层,这些层次包括:
-
链路层(数据链路层):负责物理网络上的数据传输,包括帧的封装、错误检测和纠正等。在Linux中,这一层通常与特定的网络接口卡(NIC)驱动程序相关联。
-
网络层:提供IP地址管理和路由功能,确保数据包能够正确地从一个网络传输到另一个网络。Linux支持IPv4和IPv6两种IP协议版本。
-
传输层:提供端到端的通信服务,确保数据的可靠传输或快速、不可靠的传输。TCP(传输控制协议)提供可靠的数据传输,而UDP(用户数据报协议)则提供无连接的数据传输服务。
-
应用层:提供用户和网络服务之间的接口,包括HTTP(用于Web浏览)、SMTP(用于电子邮件发送)、FTP(用于文件传输)等多种应用层协议。
一般而言:
- 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容
- 对于一台路由器,它实现了从网络层到物理层
- 对于一台交换机,它实现了从数据层到物理层
- 对于一台集线器,它只实现了物理层
传输层的典型代表:
TCP协议 | UDP协议 |
---|---|
传输层协议 | 传输层协议 |
有连接 | 无连接 |
可靠传输 | 不可靠传输 |
面向字节流 | 面向数据报 |
TCP协议格式:
- 确认应答至少应该是一个完整的TCP报头
- 确认序号 = 序号 + 1,表示该序号之前的内容被全部收到了
- 为什么要有序号和确认序号两个序号,并且是独立的字段?
TCP报文,大多数情况下既是应答,又是数据,即捎带应答机制,这个时候序号和确认序号这两个字段要被同时使用。
TCP 将每个字节的数据都进行了编号,即为序列号。每一个 ACK 都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
- 4位首部长度:这个字段的单位是4字节,取值范围是0到15,乘以4后得到报头的实际字节长度范围是20到60字节。当首部长度为5时,表示的是标准的20字节报头。
6 位标志位:用于区分报文类型
标志位 | 说明 |
---|---|
URG | 紧急指针是否有效 |
ACK | 表明自己是应答报文 |
PSH | 提示接收端应用程序立刻从 TCP 缓冲区把数据读走 |
RST | 对方要求重新建立连接,我们把携带 RST 标识的称为复位报文段 |
SYN | 请求建立连接,我们把携带 SYN 标识的称为同步报文段 |
FIN | 通知对方,本端要关闭了,我们称携带 FIN 标识的为结束报文段 |
- 16位窗口大小:流量控制,由接收缓冲区剩余空间大小决定,由滑动窗口实现
- 超时重传:在TCP连接中,当发送方发送一个数据段后,会启动一个超时计时器,如果在计时器超时之前,发送方没有收到接收方的确认(ACK)报文,那么发送方就会认为该数据段已经丢失,并重新发送该数据段,直到收到确认报文或达到重传次数限制为止。
在正常情况下,TCP 要经过三次握手建立连接,四次挥手断开连接。
为什么要三次握手?
建立双方主机通信的意愿共识,双方验证全双工信道的通畅性。
- 如果服务器不关闭sockfd,则只会完成两次挥手,服务器就会长时间处于
close_wait
状态。
UDP协议格式:
-
无连接:知道对端的 IP 和端口号就直接进行传输,不需要建立连接;
-
不可靠:没有确认机制,没有重传机制,如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
-
面向数据报:不能够灵活的控制读写数据的次数和数量;
-
16 位 UDP 长度,表示整个数据报(UDP 首部+UDP 数据)的最大长度,如果要传输的数据超过 64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
-
如果校验和出错,就会直接丢弃;
-
UDP协议的报头是固定的8字节,所以协议的接收方直接截取前8个字节的报头,剩下的就是有效数据。
UDP的缓冲区:
- 发送缓冲区:UDP 没有真正意义上的发送缓冲区,调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
- 接收缓冲区:UDP的接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致,如果缓冲区满了,再到达的 UDP 数据就会被丢弃;
2、Mac、IP地址
每台主机在局域网上,要有唯一的标识来保证主机的唯一性:mac 地址。
以太网中,任何时刻,只允许一台机器向网络中发送数据。如果有多台同时发送,会发生数据干扰,我们称之为数据碰撞,所有发送数据的主机要进行碰撞检测和碰撞避免,没有交换机的情况下,一个以太网就是一个碰撞域,局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标mac地址判定的。
其中每层都有协议,当我们进行传输流程的时候,要进行封装和解包:
Tcp/IP通讯过程:
IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址,对于 IPv4 来说, IP 地址是一个 4 字节,32 位的整数,我们通常也使用 “点分十进制” 的字符串表示 IP 地址, 例如1.94.9.200,用点分割的每一个数字表示一个字节,范围是 0 - 255。
Mac地址 vs IP地址:
唐僧从东土大唐出发,要去西天拜佛求经,途中要经过女儿国和黑风岭,女儿国和黑风岭是相邻两地。
- 东土大唐 -> 西天:源IP地址 -> 目的IP地址
- 女儿国 -> 黑风岭:源Mac地址 -> 目的Mac地址
其中经过的各个国家就是路由器,相邻的国家在同一个局域网中,路由器路由的下一个目的地是根据目的IP地址路由的,局域网通信需要Mac地址指路,一般Mac地址只在局域网中有效,IP地址几乎不变。
IP在网络中标识主机的唯一性,数据传输到主机不是目的而是手段,最终到达主机内的目的进程才是目的。但是在主机中,同一时间进程可能有很多,那怎么找到目的进程呢?
3、端口号
端口号(port)是传输层协议的内容,是一个2字节16位的整数,端口号标识唯一进程,一个端口号只能被一个进程占用。
其中 0 - 1023 是知名端口号,HTTP, FTP, SSH等这些广为使用的应用层协议,它们的端口号都是固定的。1024 - 65535 是操作系统动态分配的端口号,比如客户端程序的端口号就是有操作系统动态分配的。
pid也可以标识唯一进程,为什么还要引入端口号呢?
进程pid属于系统概念,如果继续沿用pid标识唯一进程,会增加耦合度。另外,一个进程可以绑定多个端口号,但一个端口号不能被多个进程绑定。
网络通信的本质,也是进程间通信,本质是两个互联网进程代表人来进行通信。IP + port 叫做套接字socket。
一个进程可以 bind 多个端口号,但一个端口号不能被多个进程 bind。
4、网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,网络数据流同样有大端小端之分,如何定义网络数据流的地址?
为使网络程序具有可移植性,使用样的C代码在大端和小端机器上编译后都能正常运行,可以调用下面库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t hostlong);
uint16_t ntohs(uint16_t hostshort);
h
表示host
,n
表示network
,l
表示32位长整数,s
表示16位短整数。
5、socket
socket常见API:
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
socket:
bind:
- bind()成功返回 0,失败返回-1
- bind()的作用是将参数 sockfd 和 myaddr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号
- struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度,我们可以对 myaddr 参数这样初始化:
struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY;
listen:
- listen()声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接
等待状态,如果接收到更多的连接请求就忽略- listen()成功返回 0,失败返回-1
accept:
- 三次握手完成后,服务器调用 accept()接受连接
- 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待,直到有客户端
连接上来- addr 是一个传出参数,accept()返回时传出客户端的地址和端口号
- 如果给 addr 参数传 NULL,表示不关心客户端的地址
- addrlen 参数是一个传入传出参数(value-result argument),传入的是调用者提供的,缓冲区 addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度
connect:
- 客户端需要调用 connect()连接服务器
- connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,connect 的参数是对方的地址
- connect()成功返回 0,出错返回-1
注意:
- 由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配
- 客户端不是不允许调用 bind(),只是没有必要显示的调用 bind()固定一个端口号,否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接
- 服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦
sockaddr结构:
sock API是一层抽象的网络编程接口,适用于各种底层网络协议,各种网络协议的地址格式并不相同。
socket API 可以都用struct sockaddr*
类型表示,在使用的时候需要强制转换成sockaddr_in
,增加了程序的通用性。
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~
