文章目录
前言
一、网络知识概述
1. 网路结构分层
2. socket
3. IP地址
4. 端口号
5. 字节序
二、网络编程常用API
1. socket函数
2. bind函数
3. listen函数
4. accept函数
5. connect函数
6. read和recv函数
7. write和send函数
三、TCP编程
1. TCP介绍
2. TCP通信流程
3. TCP服务端和客户端程序
前言
记录学习嵌入式Linux网络编程的知识重点与难点,若涉及版权问题请联系本人删除!
一、网络知识概述
1. 网路结构分层
①主要存在两种分层模型:OSI七层模型(理论)和TCP/IP四层模型(实际)。
②分层思想:采用分治法,将复杂问题划分为各个层级上的子问题。每一层向上提供服务,同时使用下层提供的服务。
③网络封包和拆包:如下图所示,从主机A传输数据到主机B的流程一般为:主机A的应用层封装好了传输数据后,传输层会给上层数据包添加TCP头部,网络层会给上层数据包添加IP头部,网络接口层会给上层数据包添加对应的头部和尾部(一般为CRC校验);路由器将接收到的帧去掉头部和尾部传输给上一层,传输层将解析IP地址并查表转发,然后继续封装为帧,传输到指定的主机B;主机B逐层拆包,最终在应用层获取接收到的真正数据。
2. socket
①概念:socket是一个特殊的文件描述符,用于网络通信。
②socket分类:
- 流式套接字(SOCK_STREAM):对应TCP,面向连接、可靠。
- 数据报套接字(SOCK_DGRAM):对应UDP,无连接、不可靠。
- 原始套接字(SOCK_RAW):对应多个协议,可以直接访问IP、ICMP,跨过了传输层。
3. IP地址
①概念:IP地址是网络中主机地址的标识。通过IP可以找到对应的主机。
②IP地址分类:
- IPv4:一个32位(4字节)的整数,每个字节用.来分隔。每个字节的数据范围为0~255。例如: 192.168.5.11 不够用,引入局域网可以解决不够用的问题
- IPv6:一个128(16字节)的整数,每两个字节为一部分,总共有8部分,每部分用:分隔。例如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b “地球上每一粒沙子都能分配一个”
③特殊的IP地址:
- 局域网IP:192.XXX.XXX.XXX 10.XXX.XXX.XXX
- 广播IP:XXX.XXX.XXX.255 255.255.255.255(全网广播)
- 组播IP:224.XXX.XXX.XXX到239.XXX.XXX.XXX
4. 端口号
①概念:是一个16位的整数,范围:1~65535.
②端口号分类:
- 众所周知的端口:1~1023. (FTP: 21, SSH: 22, HTTP: 80, HTTPS: 469)
- 保留端口:1024~5000 不建议使用
- 可以使用端口:5001~65535
③注意事项:
- TCP端口和UDP端口是相互独立的。(因为它们在内核中的处理路径不同)
- 网络通信由IP地址+端口号来决定。IP地址确定主机位置,端口号确定主机中哪个进程来处理。
5. 字节序
①概念:当涉及内存中存取多字节数据时,就会遇到字节序的问题。
②两种字节序:
- 小端:低字节序内容存储到低地址的内存中。
- 大端:低字节序内容存储到高地址的内存中。
③注意事项:
- 一般本地采用小端模式,网络传输采用大端模式。
- 在发送数据和接收数据前,应该先将本地字节序和网络字节序进行转换。
④主机字节序和网络字节序转换函数:
#include <arpa/inet.h>
// 主要用于网络通信过程中IP和端口的转换
uint16_t htons(uint16_t hostshort); //短整形,主机字节序->网络字节序
uint32_t htonl(uint32_t hostlong); //整形,主机字节序->网络字节序
uint16_t ntohs(uint16_t netshort); //短整形,网络字节序->主机字节序
uint32_t ntohl(uint32_t netlong); //整形,网络字节序->主机字节序
⑤IP地址转换函数:
inet_pton函数:将IP地址从主机字节序->网络字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:int inet_pton(int af, const char *src, void *dst);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,即要转换的IP地址。例如:192.168.5.11
- dst:传出参数,存放转换后的IP地址。
【4】返回值:成功返回1,第一个参数无效返回-1,第二个参数无效返回0.
inet_ntop函数:将IP地址从网络字节序->主机字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,其中存储了网络字节序的IP地址。
- dst:传出参数,存放转换后的IP地址。
- size:dst指向的内存中最多可以存储多少个字节。
【4】返回值:成功返回指针指向第三个参数对应的地址,失败返回NULL。
二、网络编程常用API
以下函数除read和write外都需包含头文件#include <sys/types.h>、#include <sys/socket.h>
1. socket函数
【1】功能:创建网络套接字。
【2】函数原型:int socket(int domain, int type, int protocol);
【3】参数说明:
domain | AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 | |
AF_LOCAL | 本地通信 | |
type | SOCK_STREAM | 流式套接字,对应TCP |
SOCK_DGRAM | 数据报套接字,对应UDP | |
SOCK_RAW | 原始套接字 | |
protocol | 一般写0即可,使用默认协议。非0一般用于原始套接字。 |
【4】返回值:成功返回套接字fd,失败返回-1。
【5】代码示例:
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket error");
return -1;
}
2. bind函数
【1】功能:将本地的IP、端口与套接字绑定。
【2】函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | struct sockaddr类型的变量的地址 | |
addrlen | addr指向的内存的地址大小 |
struct sockaddr结构体:(写数据时一般不用)
struct sockaddr { sa_family_t sa_family; // 地址族协议, ipv4 char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节) }
struct sockaddr_in结构体:(常用)一般将端口和IP地址保存在该类型的变量中,然后强转为struct sockaddr类型。(它们的大小完全相同)
struct sockaddr_in { sa_family_t sin_family; /* 地址族 */ in_port_t sin_port; /* 端口号, 2字节 -> 网路字节序 */ struct in_addr sin_addr; /* IP地址, 4字节 -> 网络字节序 */ /* 填充8字节,初始化为0 */ unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) - sizeof (in_port_t) - sizeof (struct in_addr)]; }; struct in_addr { in_addr_t s_addr; };
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; //IPv4协议
sin.sin_port = htons(SERV_PROT); //端口号,转化为网络字节序
//IP地址,转化为网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {
perror("inet_pton error");
return -1;
}
if (bind(fd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind error");
return -1;
}
3. listen函数
【1】功能:设置套接字监听。调用之前需要bind绑定。
【2】函数原型:int listen(int sockfd, int backlog);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
backlog | 同时处理的最大连接数,一般可取5,最大值为128。表示系统允许2*backlog+1个客户端同时进行三次握手。 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
if (listen(fd, 128)) {
perror("listen error");
return -1;
}
4. accept函数
【1】功能:服务器阻塞等待客户端的连接请求,建立新连接,得到通信使用的套接字。
【2】函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
addr | 传出参数,保存客户端的地址信息 | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回与客户端进行通信的套接字,失败返回-1。
【5】代码示例:
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd < 0) {
perror("accept error");
return -1;
}
5. connect函数
【1】功能:客户端发起连接请求。成功连接服务器后,客户端会自动随机绑定一个端口。
【2】函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | 存储要连接的服务端的IP和端口信息(需要网络字节序) | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;//IPv4协议
sin.sin_port = htons(SERV_PORT);//目标服务端的端口号,网络字节序
//目标服务端的IP地址,网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {
perror("inet_pton error");
return -1;
}
//调用connect函数
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
perror("connect error");
return -1;
}
6. read和recv函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t read(int sockfd, void *buf, size_t len);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 存储接收的数据 | |
len | buf指向的内存的容量 | |
flags | 特殊属性,一般不使用,指定为0 |
【4】返回值:>0表示实际接收的字节数,==0表示对方断开了连接,-1表示失败。
7. write和send函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t write(int sockfd, const void *buf, size_t len);
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 发送的数据 | |
len | buf的长度 | |
flags | 特殊属性,一般不使用,指定为0 |
【4】返回值:>0表示实际发送的字节数(与len相等),-1表示失败。
三、TCP编程
1. TCP介绍
TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:建立连接需要三次握手,断开连接需要四次挥手。
- 可靠安全:在TCP通信过程中,对每个发送的数据包都会进行校验,若数据丢失则重传。
- 流式传输:发送端和接收端的处理速度、数据量都可以不一致。
2. TCP通信流程
参考爱编程的大丙:
3. TCP服务端和客户端程序
服务端程序:接收客户端数据并打印,同时将收到的数据重新发回客户端。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 9999
int main(int argc, char **argv)
{
/* 1.创建TCP的套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("server socket error");
return -1;
}
/* 2.绑定IP和端口号 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY宏表示本机所有IP
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("server bind error");
return -1;
}
/* 3.设置监听 */
ret = listen(fd, 128);
if (ret == -1) {
perror("server listen error");
return -1;
}
/* 4.等待客户端连接 */
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd == -1) {
perror("server accept error");
return -1;
}
//打印客户端信息
char ip[24] = {0};
printf("客户端IP: %s, 端口: %d\n",
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(clientAddr.sin_port));
/* 5.与客户端通信 */
while (1) {
//接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
ret = read(cfd, buf, sizeof(buf));
if (ret == 0) {
printf("服务端:与客户端断开连接\n");
break;
} else if (ret < 0) {
perror("server read error");
break;
} else {
//打印接收数据,并重新发回接收的数据
printf("服务端接收: %s\n", buf);
write(cfd, buf, sizeof(buf));
}
}
/* 6.关闭文件描述符 */
close(cfd);
close(fd);
return 0;
}
客户端程序:每1秒发送指定数据,同时接收来自服务端的数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 9999
#define SERV_IP "192.168.124.6"
int main(int argc, char **argv)
{
/* 1.创建TCP的套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("client socket error");
return -1;
}
/* 2.与服务端建立连接 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("client connect error");
return -1;
}
//打印连接的服务端信息
printf("连接的服务端IP: %s, 端口: %d\n", SERV_IP, SERV_PORT);
/* 3.与服务端通信 */
int number = 0;
while (1) {
//发送数据
char buf[1024] = {0};
sprintf(buf, "客户端: number = %d\n", number++);
write(fd, buf, sizeof(buf));
//接收数据
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (ret == 0) {
printf("客户端:与服务端断开连接\n");
break;
} else if (ret < 0) {
perror("client read error");
break;
} else {
printf("客户端接收: %s\n", buf);
}
sleep(1);//发送数据慢一些
}
/* 4.关闭文件描述符 */
close(fd);
return 0;
}