ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

嵌入式学习[网络编程]——UDPTCP

嵌入式学习[网络编程]——UDPTCP

一、概述

1、OSI模型

OSI 模型(Open Systems Interconnection Reference Model,开放系统互连参考模型)是国际标准化组织ISO提出的网络通信框架。它把网络通信过程划分为7 层,便于理解、设计和排查网络问题。

从下到上依次是:

  1. 物理层:负责比特流在物理介质中的传输,如网线、光纤、电压、接口。
  2. 数据链路层:负责相邻节点之间的可靠传输,以“帧”为单位,典型协议/技术有 Ethernet、MAC 地址、交换机。
  3. 网络层:负责路径选择和逻辑寻址,以“包/分组”为单位,典型协议有 IP、ICMP、路由器。
  4. 传输层:负责端到端的数据传输,提供可靠传输、流量控制等,典型协议有 TCP、UDP,使用端口号。
  5. 会话层:负责建立、管理和终止通信会话。
  6. 表示层:负责数据格式转换、加密、解密、压缩等。
  7. 应用层:直接为用户提供网络服务,如 HTTP、HTTPS、DNS、FTP、SMTP。

一个常见记忆顺序是:物、数、网、传、会、表、应

2、TCP/IP 模型

TCP/IP 模型是互联网实际使用的网络通信体系结构,得名于其中最重要的两个协议:

  • TCP:传输控制协议
  • IP:网际协议

它通常分为4 层:

  1. 网络接口层:负责数据在具体网络介质上的传输,包括网卡、以太网、Wi-Fi 等。
  2. 网际层:负责逻辑寻址和路由转发,核心协议是IP,还包括 ICMP、ARP 等。
  3. 传输层:负责端到端通信,主要包括TCP和UDP。
  4. 应用层:为用户提供网络服务,包括 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/write
  • send/recv
  • close

等接口进行数据收发。

Linux中使用 socket() 函数 创建套接字,函数返回一个文件描述符(应用程序访问这个端点的句柄)

常见类型:

  • SOCK_STREAM 面向连接、可靠传输,通常对应 TCP
  • SOCK_DGRAM 无连接、数据报传输,通常对应 UDP
  • SOCK_RAW 原始套接字,可直接操作较底层的协议数据

socket函数使用:

socket(AF_INET, SOCK_STREAM, 0); // TCP socket(AF_INET, SOCK_DGRAM, 0); // UDP

4.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; }
返回列表