1.网络七层模型
OSI七层模型
|---|-------|--------------------------|-------------------------------|-----------------|
| 7 | 应用层 | 为用户提供网络服务,如网页、邮件、文件传输 | HTTP、HTTPS、DNS、SMTP、FTP、SSH | 数据 |
| 6 | 表示层 | 数据格式转换、加密解密、压缩解压 | JPEG、MPEG、ASCII、SSL/TLS(常归此层) | 数据 |
| 5 | 会话层 | 建立、管理、终止会话,对话控制、同步 | RPC、NetBIOS | 数据 |
| 4 | 传输层 | 端到端通信、端口寻址、可靠/不可靠传输、分段重组 | TCP、UDP | 段(TCP)/数据报(UDP) |
| 3 | 网络层 | 逻辑寻址、路由选择、分组转发 | IP、ICMP、路由器 | 包/分组 |
| 2 | 数据链路层 | 成帧、MAC 寻址、差错检测、介质访问控制 | 以太网、PPP、交换机、网卡 | 帧 |
| 1 | 物理层 | 传输原始比特流,定义电气、机械、过程特性 | 双绞线、光纤、无线、集线器、中继器 | 比特 |
实际应用中,有些层没必要单独存在,有些层功能被合并了,故而出现了更贴近现实的简化模型。
在四层或者五层模型中,上三层都被合并成了应用层,因为很多时候没有单出一个协议来处理表示层和会话层。
四层模型与五层模型区别在于四层模型将物理层与数据链路层合并为网络接口层,五层模型将这两层区分开也有其道理,"比特流"和"帧/MAC 地址"确实区别挺大。
套接字(Socket) 是操作系统提供的一组网络编程接口,也是网络通信中通信端点的抽象。
套接字的本质类似于一个文件、数组、结构体,其中存有一些有关网络身份的信息:
1.协议:TCP / UDP / 原始套接字
2.本地 IP 地址
3.本地端口
4.远端 IP 地址
5.远端端口
2.套接字(socket)创建函数
int socket(int domain, int type, int protocol);
功能:
创建通信的一端
参数:
@domain:
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
@type:
SOCK_STREAM //流式套接字(对应TCP协议)
Provides sequenced, reliable, two-way, connection-based byte streams.
An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM //数据报套接字 (对应UDP协议)
Supports datagrams
(connectionless,
unreliable messages of a fixed maximum length).
@protocol
协议 --- tcp
--- upd
给 0 表示默认协议
返回值:
成功 返回socket的文件描述符
失败 -1 && errno
3.套接字绑定IP和端口(bind)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:
给sockfd 绑定一个名字 (地址信息)
参数:
@sockfd --- 要当定的socket
@addr --- 绑定服务器自己的地址
@addrlen --- 服务器地址大小
返回值:
成功 0
失败 -1 && errno
函数需要的输入是一个**struct sockaddr ***类型的指针,但是实际上IPV4协议需要sockaddr_in,它们的大小是相同的,内容几乎相同
struct sockaddr {
sa_family_t sa_family; // 第一个字段
char sa_data[14];
};
struct sockaddr_in {
sa_family_t sin_family; // 和 sa_family 位置对应
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct sockaddr_in6 {
sa_family_t sin6_family; // 也是第一个字段
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
根据需要向结构体内填入需要绑定的本机IP(本机IP有多个)和端口,IP可以填入0.0.0.0来接收所有本机IP(不能接收所有端口)
sa_family_t sin6_family;其中需要填入对应的协议,其实socket中就已经有相关信息了(协议),此处相当于做校验而非选择协议。
4.令套接字进入监听状态(listen)
int listen(int sockfd, int backlog);
功能:
将sockfd设置为监听套接字(被动套接字)
参数:
@sockfd --- 要设置的socket
@backlog --- 表示等待队列的大小
返回值:
成功 0
失败 -1 && errno
注意listen函数是非阻塞的,也就是说它不负责监听,而是将套接字状态设置为只能用于接受连接,告诉内核可以开始在这个套接字上处理客户端连接请求。(真正接收连接的是accept)
5.接受连接(accept)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
从监听套接字的"已完成连接队列"中取出一个客户端连接,并创建一个新的、已连接的套接字。
参数:
@sockfd --- 监听套接字,也就是前面 listen 过的那个 socket
@addr --- 输出参数,返回客户端的地址信息;不关心可填 NULL
@addrlen --- 值-结果参数,传入 addr 缓冲区大小,返回实际地址长度;addr 为 NULL 时可填 NULL
返回值:
成功 返回新的已连接 socket 文件描述符
失败 -1 && errno
注意:
accept 默认是阻塞的,没有新连接时会卡住等待。
监听套接字 sockfd 仍然继续监听,真正和客户端通信的是 accept 返回的新 socket。
一般 TCP 服务端使用;UDP 不需要 accept。
如果被信号打断,可能返回 -1 && errno == EINTR。
6.发起连接(connect)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
TCP:主动向目标服务器发起连接,触发三次握手。
UDP:不建立真正连接,只是给套接字设置一个默认远端地址。
参数:
@sockfd --- 要发起连接的 socket
@addr --- 目标地址,通常是服务器地址
@addrlen --- 地址结构体大小
返回值:
成功 0
失败 -1 && errno
注意:
TCP 阻塞模式下,connect 会等待三次握手完成;非阻塞模式下可能返回 -1 && errno == EINPROGRESS,需要后续用 select / poll 判断是否连接成功。
UDP 调用 connect 后,可以使用 send / recv,并且只接收该默认对端发来的数据。
客户端通常不需要 bind,内核会自动分配本地 IP 和临时端口。
7.向指定地址发送数据(sendto)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:
向 dest_addr 指定的目标地址发送数据,常用于 UDP。
参数:
@sockfd --- 发送数据的 socket
@buf --- 待发送数据缓冲区
@len --- 待发送数据长度
@flags --- 通常填 0,也可用 MSG_DONTWAIT 等
@dest_addr --- 目标地址;UDP 必须填,TCP 一般填 NULL
@addrlen --- 目标地址长度
返回值:
成功 返回实际发送的字节数
失败 -1 && errno
注意:
UDP 是无连接的,所以每次发送都要指定目标地址。
UDP 发送成功只表示数据交给了内核,并不代表对方一定收到。
UDP 可能丢包、乱序、重复,且每个数据报有最大长度限制。
TCP 已连接后一般使用 send,而不是 sendto。
8.接收数据并获取来源地址(recvfrom)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:
接收数据,并在 src_addr 中返回发送方的地址信息,常用于 UDP。
参数:
@sockfd --- 接收数据的 socket
@buf --- 接收缓冲区
@len --- 缓冲区大小
@flags --- 通常填 0
@src_addr --- 输出参数,返回发送方地址;不关心可填 NULL
@addrlen --- 值-结果参数,传入 src_addr 缓冲区大小,返回实际地址长度
返回值:
成功 返回实际接收的字节数
失败 -1 && errno
注意:
TCP 中 recvfrom 返回 0 表示对端关闭连接;UDP 中返回 0 表示收到一个长度为 0 的数据报。
UDP 一次 recvfrom 通常读取一个完整数据报,缓冲区不够时多余部分可能被丢弃。
TCP 是字节流,没有消息边界,不能假设一次 recv 就对应一次 send。
9.关闭套接字(close)
int close(int fd);
功能:
关闭文件描述符,释放套接字资源。
参数:
@fd --- 要关闭的套接字文件描述符
返回值:
成功 0
失败 -1 && errno
注意:
TCP 调用 close 通常会触发四次挥手,主动关闭方可能进入 TIME_WAIT 状态。
如果多个进程通过 fork 或 dup 共享同一个套接字,close 只是让引用计数减一,全部关闭后才真正释放。
如果只想关闭读写方向,而不是完全释放套接字,可以使用 shutdown。
UDP 无连接,close 后直接释放即可。
10.TCP 与 UDP 的区别总结

典型流程:
TCP 服务端:
socket -> bind -> listen -> accept -> recv / send -> close
TCP 客户端:
socket -> connect -> send / recv -> close
UDP 服务端:
socket -> bind -> recvfrom / sendto -> close
UDP 客户端:
socket -> sendto / recvfrom -> close
也可以先 connect 设置默认对端,再使用 send / recv。
补充注意:
填写 IP 和端口时要注意网络字节序,端口通常用 htons,IPv4 地址常用 inet_pton 或 htonl。
使用 IPv4 时,实际填充的是 struct sockaddr_in,传给 bind、connect、accept、sendto、recvfrom 时通常强转为 struct sockaddr *。