一、概述
1、OSI模型
OSI 模型(Open Systems Interconnection Reference Model,开放系统互连参考模型)是国际标准化组织ISO提出的网络通信框架。它把网络通信过程划分为7 层,便于理解、设计和排查网络问题。
从下到上依次是:
- 物理层:负责比特流在物理介质中的传输,如网线、光纤、电压、接口。
- 数据链路层:负责相邻节点之间的可靠传输,以“帧”为单位,典型协议/技术有 Ethernet、MAC 地址、交换机。
- 网络层:负责路径选择和逻辑寻址,以“包/分组”为单位,典型协议有 IP、ICMP、路由器。
- 传输层:负责端到端的数据传输,提供可靠传输、流量控制等,典型协议有 TCP、UDP,使用端口号。
- 会话层:负责建立、管理和终止通信会话。
- 表示层:负责数据格式转换、加密、解密、压缩等。
- 应用层:直接为用户提供网络服务,如 HTTP、HTTPS、DNS、FTP、SMTP。
一个常见记忆顺序是:物、数、网、传、会、表、应
2、TCP/IP 模型
TCP/IP 模型是互联网实际使用的网络通信体系结构,得名于其中最重要的两个协议:
- TCP:传输控制协议
- IP:网际协议
它通常分为4 层:
- 网络接口层:负责数据在具体网络介质上的传输,包括网卡、以太网、Wi-Fi 等。
- 网际层:负责逻辑寻址和路由转发,核心协议是IP,还包括 ICMP、ARP 等。
- 传输层:负责端到端通信,主要包括TCP和UDP。
- 应用层:为用户提供网络服务,包括 HTTP、HTTPS、DNS、FTP、SMTP 等。
3、IP地址的组成
IP地址分为 IPv4 地址和 IPv6 地址,IP地址通常由网络号和主机号两部分组成。
IPv4地址:长32 位,通常写成 4 个十进制数,例:192.168.1.10 每部分范围是0~255
IPv6地址:长128 位,通常写成 8 组十六进制数,
例:2001:0db8:85a3:0000:0000:8a2e:0370:7334
4、常用名词解释
4.1. Socket 套接字
Socket 是操作系统内核提供的一种网络通信接口和通信端点。它把网络通信抽象成类似文件的操作,因此可以使用:
read/writesend/recvclose
等接口进行数据收发。
Linux中使用 socket() 函数 创建套接字,函数返回一个文件描述符(应用程序访问这个端点的句柄)
常见类型:
- SOCK_STREAM 面向连接、可靠传输,通常对应 TCP
- SOCK_DGRAM 无连接、数据报传输,通常对应 UDP
- SOCK_RAW 原始套接字,可直接操作较底层的协议数据
socket函数使用:
socket(AF_INET, SOCK_STREAM, 0); // TCP socket(AF_INET, SOCK_DGRAM, 0); // UDP4.2 IP地址+端口号
网络通信中,通常需要 IP地址+端口号 来确定一个通信端口。
IP地址用来解决数据要发送哪个主机,端口号用来解决数据要传给主机上的哪个程序。
一个 IP 地址上可以同时运行多个网络程序,端口号用来区分它们。
IP + 端口的完整含义:
例如:192.168.1.10:8080
表示:IP:192.168.1.10 端口:8080
4.3 端口号的范围
端口号是一个16 位无符号整数,所以理论范围是:0 ~ 65535,不过0通常具有特殊含义,实际使用的端口通常从1开始,故端口号范围为 1 ~ 65535。
常见端口号划分:
| 范围 | 类型 | 说明 |
|---|---|---|
0~1023 | 知名端口 | 通常分配给系统服务,如 80、443、22 |
1024~49151 | 注册端口 | 分配给常见应用程序 |
49152~65535 | 动态/私有端口 | 客户端程序临时使用 |
- 一个端口同一时间通常只能被一个程序绑定,但具体还取决于 Socket 的绑定选项。
- 端口号不是应用程序本身,而是应用程序绑定或连接的通信入口。
4.4 字节序
4.4.1概述
计算机中的数据通常以多个字节表示,例如一个 32 位整数:
0x12345678它占 4 个字节。不同 CPU 在内存中排列这些字节的顺序可能不同,而排列的顺序称为字节序。
- 大端存储
大端模式中,高位字节存放在低地址:
内存地址: 低 ----------------> 高 数据字节: 12 34 56 78这称为Big Endian。
- 小端存储
小端模式中,低位字节存放在低地址:
内存地址: 低 ----------------> 高 数据字节: 78 56 34 12这称为Little Endian。
4.4.2 网络字节序
TCP/IP 协议规定网络协议字段使用大端格式
4.4.3主机字节序
主机字节序是当前主流计算机 CPU 使用的字节序,通常使用小端存储。
4.4.4转换字节序
如果不同主机直接把整数发给对方,可能出现字节顺序不一致。
例如:
发送端发送整数:0x12345678
小端主机在内存中可能存成:78 56 34 12
而网络协议规定传输时使用大端:12 34 56 78
如果不转换,接收方可能把它解析成完全不同的数值。
常用转换函数:
uint16_t htons(uint16_t hostshort); uint16_t ntohs(uint16_t netshort); uint32_t htonl(uint32_t hostlong); uint32_t ntohl(uint32_t netlong);含义:
h = host,主机;to = to,转换到;n = network,网络;s = short,16 位;l = long,32 位
二、UDP
1、概述
UDP(User Datagram Protocol):用户数据报协议,是 TCP/IP 模型传输层中的一个无连接传输协议。它位于 IP 层之上、应用层之下,主要负责将数据从一个应用程序发送到另一个应用程序。
数据报特点: 1.数据与数据之间有边界
2.发送和接收的次数要对应
3.没有写阻塞,但发送太快,会导致数据丢失。
4.会发生读阻塞
UDP 的核心特点是:简单、开销小、速度快,但不保证可靠传输。
2、UDP 的通信过程
2.1 UDP 服务端
提供服务的一端 1个
大致过程: 1. 创建 UDP Socket
2. bind 本地 IP 和端口
3. recvfrom 接收数据
4. sendto 发送响应
5. close
2.2 UDP 客户端
使用服务的一端 可以是多个
大致过程: 1. 创建 UDP Socket
2. sendto 发送数据
3. recvfrom 接收响应
4. close
3、相关函数
3.1 socket
int socket(int domain, int type, int protocol)功能::程序向内核提出创建一个基于内存的套接字描述符
参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
PF_UNIX == AF_UNIX ==>单机程序
type 套接字类型:
SOCK_STREAM 流式套接字 ===》TCP
SOCK_DGRAM 用户数据报套接字===>UDP
SOCK_RAW 原始套接字 ===》IP
protocol 协议 ==》0 表示自动适应应用层协议。
返回值:成功 返回申请的套接字id
失败返回-1
3.2 bind
int bind(int sockfd, struct sockaddr *my_addr,socklen_t addrlen);功能:如果该函数在服务器端调用,则表示将参数1相关的文件描述符文件与参数2 指定的接口地址关联,用于从该接口接受数据。
参数:sockfd, 需要被绑定 ip 和端口号的套接字
my_addr ipv4 对应的地址结构体,man 7 ip
struct _sockaddr_in ///网络地址结构 { u_short sin_family; ////地址族 u_short sin_port; ///地址端口 struct in_addr sin_addr; ///地址IP };地址端口需要大小端转换函数(htons );
IP地址 需要使用inet_addr( )函数将IPv4 地址字符串转换为网络字节序二进制地址。
socklen_t addrlen: 参数2 的长度。
返回值:成功 0
失败 -1
3.3 sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);功能:用于UDP协议中向对方发送数据。
参数:sockfd 本地的套接字id
buff 本地的数据存储,一般是要发送的数据。
len 要发送的数据长度 (字符串通常使用strlen)
flags 要发送数据方式,0 表示阻塞发送。
dest_addr: 必选,表示要发送到的目标主机信息结构体。
addrlen :目标地址长度。
返回值:成功 发送的数据长度
失败 -1;
3.4recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);功能:用于UDP协议中获取对方发送的数据。
参数:sockfd 本地的套接字id
buff 要存储数据的内存区,一般是数组或者动态内存。
len 要获取的数据长度,一般是buff的大小。
flags 获取方式,0 阻塞
src_addr 可选,表示对方的地址信息结构体,
如果为NULL,表示不关心对方地址。
addrlen 对方地址信息结构体大小。
如果对方地址是NULL,则该值也为NULL。
返回值:成功 接收到的数据长度
失败 -1;
代码示例:
服务端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <time.h> typedef struct sockaddr *(SA); int main(int argc, char **argv) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (-1 == sockfd) { perror("socket"); return 1; } struct sockaddr_in server, client; bzero(&server, sizeof(server)); bzero(&client, sizeof(client)); // man 7 ip server.sin_family = AF_INET; server.sin_port = htons(50000); // host to net short server.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx"); int ret = bind(sockfd, (SA)&server, sizeof(server)); if (-1 == ret) { perror("socket"); return 1; } while (1) { char buf[512] = {0}; socklen_t len = sizeof(client); recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&client, &len); printf("cli:%s\n", buf); time_t tm; time(&tm); sprintf(buf, "%s %s", buf, ctime(&tm)); sendto(sockfd, buf, strlen(buf), 0, (SA)&client, len); } close(sockfd); return 0; }客户端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <time.h> typedef struct sockaddr *(SA); int main(int argc, char **argv) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (-1 == sockfd) { perror("socket"); return 1; } struct sockaddr_in server; bzero(&server, sizeof(server)); // man 7 ip server.sin_family = AF_INET; server.sin_port = htons(50000); // host to net short server.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx"); while (1) { char buf[512] = "hello,this is udp test"; sendto(sockfd, buf, strlen(buf), 0, (SA)&server, sizeof(server)); bzero(buf, sizeof(buf)); int ret = 0; recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL); printf("ser:%s", buf); fflush(stdout); sleep(1); } }三、TCP
1、概述
TCP(Transmission Control Protocol)是 TCP/IP 模型中的传输层协议
通信前必须先建立连接:
客户端 -> 服务器:SYN
服务器 -> 客户端:SYN + ACK
客户端 -> 服务器:ACK
这就是常说的三次握手。
通信结束后还要释放连接,通常涉及四次挥手。
2、模型
2.1 C/S 模型
基本概念 :C = Client,客户端
S = Server,服务器
C/S 模型就是:专用客户端 <------网络------> 服务器
2.2 B/S模型
基本概念:B = Browser,浏览器
S = Server,服务器
B/S 模型就是:浏览器 <------网络------> Web 服务器
用户不需要安装专用客户端,只需要使用浏览器访问网页。
2.3 P2P模型
基本概念:P2P = Peer to Peer (对等网络 点对点网络)
P2P 中每个节点既可以下载数据,也可以上传数据,即在 P2P 网络中,每个节点既是下载者,也是上传者
典型应用:迅雷下载,部分游戏联机网络
3、特点
3.1 面向连接
TCP通信前必须三次握手,通信结束后需要四次挥手结束
3.2 可靠传输
TCP 通过以下机制尽量保证数据正确到达:
- 序号:标记数据的位置
- 确认应答:接收端确认已经收到数据
- 超时重传:数据丢失后重新发送
- 校验和:检测数据是否损坏
- 去重:丢弃重复数据
- 重排:按照序号恢复正确顺序
3.3 面向字节流
TCP 把数据看作连续的字节流,不保留发送时的数据边界,通常需要自己定义消息格式。
3.4 全双工通信
TCP 连接建立后,双方可以同时发送和接收数据
3.5 点对点通信
TCP 通常是一对一通信:
一个发送端 <---- TCP 连接 ----> 一个接收端
TCP 本身不直接支持广播和组播,通常用于单播通信
3.6 提供流量控制和拥塞控制
4、相关函数
4.1 socket
与UDP相同
4.2 bind
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);功能:如果该函数在服务器端调用,则表示将参数1相关 的文件描述符文件与参数2 指定的接口地址关联, 用于从该接口接受数据。
4.3 listen
int listen(int sockfd, int backlog);功能:在参数1所在的套接字id上监听等待链接。
参数:sockfd 套接字
id backlog 允许三次握手的排队数。
返回值:成功 0 失败 -1;
4.4 accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);功能:从已经监听到的队列中取出有效的客户端链接并 接入到当前程序。
参数:sockfd 套接字id
addr 如果该值为NULL ,表示不论客户端是谁都接入。
如果要获取客户端信息,则事先定义变量 并传入变量地址,
函数执行完毕将会将客户端 信息存储到该变量中。
addrlen: 参数2的长度,如果参数2为NULL,则该值 也为NULL;
如果参数不是NULL,&len; 一定要写成len = sizeof(struct sockaddr);
返回值:成功 返回一个用于通信的新套接字id; 通信套接字 从该代码之后所有通信都基于该id
失败 -1;
4.5 recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);功能:从指定的sockfd套接字中以flags方式获取长度为len字节的数据到指定的buff内存中。
参数:sockfd 如果服务器则是accept的返回值的新fd 如果客户端则是socket的返回值旧fd
buff 用来存储数据的本地内存,一般是数组或者 动态内存。
len 要获取的数据长度
flags 获取数据的方式,0 表示阻塞接受。
返回值:成功 表示接受的数据长度,一般小于等于len
失败 -1;
4.6 send
int send(int sockfd, const void *msg, size_t len, int flags);功能:从msg所在的内存中获取长度为len的数据以flags方式写入到sockfd对应的套接字中。
参数:sockfd: 如果是服务器则是accept的返回值新fd 如果是客户端则是sockfd的返回值旧fd
msg 要发送的消息
len 要发送的消息长度
flags 消息的发送方式。
返回值:成功 发送的字符长度
失败 -1;
4.7 connect
客户端 自动连接服务器。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);功能:该函数固定有客户端使用,表示从当前主机向目标 主机发起链接请求。
参数:sockfd 本地socket创建的套接子id
addr 远程目标主机的地址信息。
addrlen: 参数2的长度。
返回值:成功 0 失败 -1;
注意:
1、服务端流程:socket---->bind---->listen---->accept---->recv---->send---->close
客户端流程:socket---->connect---->send---->recv---->close
2、三次握手中的函数:
listen() = 让内核准备接收连接 ——服务端
connect() = 让内核主动发起连接 ——客户端
accept() = 获取已经建立的连接 ——服务端
三次握手 = TCP 内核自动完成3、服务端accep获取的为通信套接字,通信套接字已经包含客户端的地址,后续recv与send只需要使用通信套接字即可,不用像UDP一样传输地址。
实例代码:
服务端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <time.h> #include <arpa/inet.h> typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 监听套接字 int listfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == listfd) { perror("socket"); return 1; } struct sockaddr_in ser, cli; bzero(&ser, sizeof(ser)); bzero(&cli, sizeof(cli)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); ser.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx"); int ret = bind(listfd, (SA)&ser, sizeof(ser)); if (-1 == ret) { perror("bind"); return 1; } listen(listfd, 3); //最多容纳 3 个等待处理的连接 socklen_t len = sizeof(cli); //通信套接字 后续接收都用通信套接字 int conn = accept(listfd, (SA)&cli, &len); if (-1 == conn) { perror("accept"); return 1; } while (1) { char buf[256] = {0}; int ret = recv(conn, buf, sizeof(buf), 0); // conn包含地址,不需要像udp一样传地址 if (ret <= 0) { printf("cli offline\n"); break; } time_t tm; time(&tm); sprintf(buf, "%s %s", buf, ctime(&tm)); send(conn, buf, strlen(buf), 0); } close(listfd); close(conn); return 0; }客户端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <time.h> #include <netinet/in.h> #include <arpa/inet.h> #define SENDIP "xxx.xxx.xxx.xxx" typedef struct sockaddr *(SA); int main(int argc, char **argv) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("socket"); return 1; } struct sockaddr_in ser; bzero(&ser, sizeof(ser)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); ser.sin_addr.s_addr = inet_addr(SENDIP); int ret = connect(sockfd, (SA)&ser, sizeof(ser)); if (-1 == ret) { perror("connect"); return 1; } while (1) { char buf[256] = "tcp test......"; send(sockfd, buf, strlen(buf), 0); bzero(buf, sizeof(buf)); recv(sockfd, buf, sizeof(buf), 0); printf("%s", buf); sleep(1); } close(sockfd); return 0; }