网络基础
网络体系结构
定义:是指网络的分层模型和每层所使用的协议的集合
有两种网络体系结构:OSI和TCP/IP
OSI七层模型
TCP/IP四层模型
对于以上两种模型来说,除了物理层没有协议,其他各 层都有对应的协议去完成该层相应的功能。
IP地址
IP地址: 在网络中唯一标识一台主机的符号
唯一标识一台主机的符号是MAC地址(硬件地址)
ipv4维护的IP地址宽度是4个字节;ipv6是16个字节。
4个字节的IP地址可表示的地址范围是:
0000 0000*4~1111 1111*4
可以分成5类:
A类:1.0.0.1~126.255.255.254
B类:127.0.0.1~191.255.255.254
C类:192.0.0.1~223.255.255.254--->用户地址
D类:224.0.0.1~239.255.255.254--->组播&多播地址
E类:240以后用于特殊的网络协议和应用程序
C类=24bit网络号+8bit主机号
网络协议
定义:通信双方对某种通信规则的约定
协议:
通用的网络协议 TCP/IP协议栈(OS) 行业内部专用协议 自定义协议
端口号
作用:用来区分应用进程
端口号是一个无符号的整数,范围:1~65535(其中1~1023已经被占用)
字节序
主机字节序:是指不同的CPU主机存贮多字节整数的方 式,有大端序主机和小端序主机
大端序主机:数据的高字节存放在内存的低地址
小端序主机:数据的高字节存放在内存的高地址
主机字节序-->网络字节序
多字节整数发送前要从主机字节序转化成网络字节序, 以适配对端的网络主机(Htonl和Htons)
网络字节序:大端序
两种协议
基于TCP协议的网络客户端和服务端模型
基于TCP协议的网络客户端和服务端模型是一种常见的网络通信模型,它使用TCP协议作为通信协议来实现可靠的数据传输。
在这个模型中,客户端和服务端通过TCP连接进行通信。客户端首先与服务端建立连接,然后可以发送请求给服务端,服务端接收请求并给予响应。客户端和服务端之间可以进行多次的请求和响应交互。
服务端:socket-->bind-->listen-->accept-->IO函数(send/recv)
客户端:socket-->connect-->IO函数
下面是一个基于TCP协议的网络客户端和服务端模型的示例代码:
服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define SIZE_BUF 1024
int main() {
int serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(struct sockaddr_in);
char buf[SIZE_BUF] = {0};
// 创建套接字
if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
// 绑定地址和端口
if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听新的连接请求
if (listen(serverSocket, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while(1) {
// 接受新的连接
if ((clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
printf("New client connected: %s:%d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
// 处理客户端请求
memset(buf, 0, SIZE_BUF);
if (recv(clientSocket, buf, SIZE_BUF, 0) < 0) {
perror("recv failed");
close(clientSocket);
continue;
}
printf("Received command from client: %s\n", buf);
// TODO: 根据客户端的命令进行处理
// 关闭连接
close(clientSocket);
}
// 关闭服务器套接字
close(serverSocket);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define SIZE_BUF 1024
int main() {
int clientSocket;
struct sockaddr_in serverAddr;
char buf[SIZE_BUF] = {0};
// 创建套接字
if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 将IP地址从文本转换成二进制格式
if (inet_pton(AF_INET, SERVER_IP, &(serverAddr.sin_addr)) <= 0) {
perror("Invalid address/Address not supported");
exit(EXIT_FAILURE);
}
// 连接服务器
if (connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
printf("Connected to server: %s:%d\n", SERVER_IP, PORT);
// 发送命令给服务器
strcpy(buf, "Hello, server!");
if (send(clientSocket, buf, SIZE_BUF, 0) < 0) {
perror("send failed");
close(clientSocket);
exit(EXIT_FAILURE);
}
// TODO: 接收服务器的响应并处理
// 关闭连接
close(clientSocket);
return 0;
}
基于UDP协议的客户端和服务端模型
基于UDP协议的客户端和服务端模型是另一种常见的网络通信模型。在这个模型中,客户端和服务端使用UDP协议进行通信,数据包通过无连接的方式进行传输。
服务端:socket-->bind-->IO函数(recvfrom/sendto)
客户端:socket-->IO函数(sendto/recvfrom)
下面是一个基于UDP协议的客户端和服务端模型的示例代码:
服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define SIZE_BUF 1024
int main() {
int serverSocket;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(struct sockaddr_in);
char buf[SIZE_BUF] = {0};
// 创建套接字
if ((serverSocket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
// 绑定地址和端口
if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while(1) {
// 接收客户端数据
memset(buf, 0, SIZE_BUF);
ssize_t numBytes = recvfrom(serverSocket, buf, SIZE_BUF, 0, (struct sockaddr *)&clientAddr, &addrLen);
if (numBytes < 0) {
perror("recvfrom failed");
continue;
}
printf("Received message from client: %s\n", buf);
// TODO: 根据客户端的请求进行处理
// 发送响应给客户端
if (sendto(serverSocket, buf, numBytes, 0, (struct sockaddr *)&clientAddr, addrLen) < 0) {
perror("sendto failed");
continue;
}
}
// 关闭服务器套接字
close(serverSocket);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define SIZE_BUF 1024
int main() {
int clientSocket;
struct sockaddr_in serverAddr;
char buf[SIZE_BUF] = {0};
// 创建套接字
if ((clientSocket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
// 将IP地址从文本转换成二进制格式
if (inet_pton(AF_INET, SERVER_IP, &(serverAddr.sin_addr)) <= 0) {
perror("Invalid address/Address not supported");
exit(EXIT_FAILURE);
}
printf("Connected to server: %s:%d\n", SERVER_IP, PORT);
// 发送消息给服务器
strcpy(buf, "Hello, server!");
if (sendto(clientSocket, buf, SIZE_BUF, 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("sendto failed");
close(clientSocket);
exit(EXIT_FAILURE);
}
// 接收服务器的响应
memset(buf, 0, SIZE_BUF);
socklen_t addrLen = sizeof(struct sockaddr_in);
if (recvfrom(clientSocket, buf, SIZE_BUF, 0, (struct sockaddr *)&serverAddr, &addrLen) < 0) {
perror("recvfrom failed");
close(clientSocket);
exit(EXIT_FAILURE);
}
printf("Received response from server: %s\n", buf);
// 关闭套接字
close(clientSocket);
return 0;
}
两种网络编程框架的对比
特点
TCP:使用流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内部设置了流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
UDP:使用数据报套接字(SOCK_DGRAM)
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)可以对较低层次协议如IP、ICMP直接访问
协议
TCP(传输控制协议):
TCP是一个可靠的,全双工的,有序的,面向链接的字节流通信的协议。
为什么可靠:
1.丢失的数据包重发 (能保证拿到数据)
2.错误的数据包重发 (保证能拿到正确的数据)
3.数据的有序到达(因为对每个数据包进行了编号)(拆包,编 号)
4.有较为健全的校验机制(为了保证数据的正确性)
5.支持面向连接(保证通信线路的畅通)-->三次握手
6.有信道拥堵控制(通过一种对于信道拥堵解决的方案,来提高转发效率)
为什么有序(有序列号):
1.保证数据都能传输给对端,不至于当传输的数据 > 信道带宽,导致数据丢弃。
2.通过序号,在对端主机上可以拼接成原本的数据包
3.保证数据传输的可靠性
如何面向链接:
三次握手和四次挥手
UDP:
UDP(The User Datagram Protocol):无连接的数据报协议,别名"不可靠的协议"
<1>使用校验和来实现错误侦测
<2>UDP常用于媒体流的传输(音频、视频、等),在这种情况下,实时性比可靠性更重要
<3>UDP也常用于简单的查询/回应程序,例如DNS查找,在这种情况下,建立可靠传输的资源消耗太大
<4>UDP是一种实时传输协议(Real-time Transport Protocol),这种协议通常用来传输实时数据例如:音视频流
不可靠的原因:
1.非面向连接(不关心接收端是否在线)-->没有三次握手
2.丢包不重发(可以通过重传来解决)
3.错误的包不重发
4.没有信道拥堵控制
5.有一个最大传输长度限制
6.没有严格的校验机制
如何抉择使用TCP还是UDP?
1.可靠性
2.实时性
可靠性 > 实时性: TCP
可靠性 < 实时性: UDP
IO模型
阻塞IO 非阻塞IO IO多路复用 异步IO
阻塞IO
若要读的数据没有准备好,或要写入的目标没有空间,将会发生读写阻塞
常见的阻塞IO函数:
recv recvfrom read write send accept connect
非阻塞IO
若要读的数据没有准备好,IO函数返回一个约定的错误值,不阻塞当前进程 ,可以通过循环去多次尝试读取数据。
常见的非阻塞IO函数:
read/write(可以通过修改底层驱动设计成非阻塞)
IO多路复用
I/O多路复用是一种同时监控多个I/O流的机制,它能够在一个线程中同时处理多个I/O操作。常见的I/O多路复用模型有select、poll和epoll。
这些模型可以用于在一个线程中同时监听多个文件描述符的可读、可写或异常等事件,并在事件发生时进行相应的处理。
服务器模型
服务器:可以服务于多个客户端的服务程序
种类:循环服务器和并发服务器
循环服务器:指的是该服务程序可以服务于多个客户端, 但是在同一时刻只能服务一个客户端。while
并发服务器:指的是该服务器可以在同一时刻服务于多个 客户端
在linux系统中,实现并发服务器,可以使用:
多进程 多线程 select poll epoll
多进程
思想:主进程专门用于连接多个客户端,若有一个客户端接进来,就创建一个子进程来负责该客户端的业务数据的收发 。
多线程
思想:主进程专门用于连接多个客户端,若有一个客户端接进来,就创建一个子线程来负责该客户端的业务数据的收发 。
pthread_create(&tID,NULL,ThreadFunc,(void *)&iClient)
参数1:创建的线程
参数2:指定线程的属性,NULL表示使用缺省属性
参数3:线程执行的函数-->所指向的函数是一个返回值为(void *),参数也为(void*)的函数
参数4:传递给线程执行的函数的参数,参数类型是(viod *)
select-->IO多路复用
思想:
1 构建一张文件描述符集合表,表的大小为1024bit,这1024个bit位用来存放1024个文件描述符对应的IO通道是否有数据发生,有数据发生的通道对应的BIT位置1,没数据发生的通道对应的BIT位置0。-->空表
2 使用select函数去监控关注的文件描述符对应的通道是否有数据发生?若监控到一路或多路通道有数据发生,则返回通道的路数同时将有数据发生的通道对应的文件描述符集合表的相应BIT位置1,同时将其他BIT位置0
(监控 置位)
3 对文件描述符集合表中的置位结果做出判断和响应。使用FD_ISSET去判断关心的描述符是否被置位了,若被置位了,返回真,那么响应这路IO;若没有被置位,返回假,不做处理。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
作用:监控文件描述符对应的IO通道是否有数据发生
参数1:监控的最大通道个数
参数2:指向读文件描述符集合表的指针
参数3:指向写文件描述符集合表的指针
参数4:指向异常文件描述符集合表的指针
参数5:timeout=0,只检测文件描述符集合的状态,然 后立即返回
timeout=NULL,select函数阻塞监控关注的文件 描述符通道
timeout=时间,时间到达之间select函数一置阻 塞监控,时间到,结束监控。
返回值:成功返回监控到有数据发生的IO通道的个数
失败 -1
poll模型
poll函数与select函数类似,也可以同时监听多个文件描述符,但是其内部实现方式不同。使用poll函数时,需要创建一个pollfd数组来存储要监听的文件描述符以及关注的事件类型。poll函数会阻塞,直到有文件描述符就绪,然后将就绪的文件描述符以及对应的事件返回。
epoll模型
epoll是Linux特有的高性能I/O多路复用机制。在epoll模型中,使用epoll_create函数创建一个epoll对象,然后调用epoll_ctl函数注册要监听的文件描述符和事件类型。在事件发生时,可以通过调用epoll_wait函数获取就绪的文件描述符以及事件。
网络服务器超时检测
select超时设置
在使用select函数进行I/O多路复用时,可以通过设置超时时间来限定select的阻塞时间,以避免无限期等待。以下是使用select进行超时设置的基本步骤:
创建并初始化fd_set结构体:首先需要创建一个fd_set结构体对象,并将要监听的文件描述符添加到该结构体中,以准备进行监控。
设置超时时间:使用struct timeval结构体来设置超时时间。这个结构体包含两个成员变量,分别是tv_sec表示秒数、tv_usec表示微秒数。
调用select函数:将创建的fd_set对象和超时时间作为参数传递给select函数。
检查返回值:select函数会阻塞等待,直到有文件描述符准备就绪或超时发生。当select返回时,可以通过判断返回值来确定是否有就绪的文件描述符。
处理就绪事件:根据返回值判断哪些文件描述符已经准备就绪,然后进行相应的读写操作等处理。
使用信号和alarm
使用信号和alarm函数来设置超时,可以按照以下步骤进行操作:
注册信号处理函数:首先,需要注册一个信号处理函数,用于处理超时事件。可以使用signal或sigaction函数来注册信号处理函数。在处理函数中,可以进行超时后的相应操作,例如关闭连接、释放资源等。
设置超时时间:使用alarm函数设置超时时间,它以秒为单位。当超过指定秒数后,系统会发送一个SIGALRM信号给进程。
执行需要超时检测的操作:在调用alarm函数之前,执行需要进行超时检测的操作,例如网络通信、文件读写等。alarm函数在指定的超时时间到达后,会发送一个SIGALRM信号,中断正在执行的操作。
处理信号:当收到SIGALRM信号时,会调用事先注册的处理函数进行处理。
设置socket属性
使用setsockopt函数设置套接字接收超时(SO_RCVTIMEO)或发送超时(SO_SNDTIMEO)选项。以下是基本步骤:
创建套接字并初始化。
设置超时时间。
使用setsockopt函数设置相应的选项。
进行套接字的读写操作,在超时时间内检查返回值以确定是否超时。
广播&组播
在网络中,数据包的发送方式有三种:单播 广播 组播
广播
数据发送方将数据包发送给了局域网中的所有的主机。
广播地址通常是这个网段中最大的IP地址,例如192.168.12.255或者是255.255.255.255
广播发送:socket-->setsockopt(允许广播发送) --> sendto(广播地 址) udp_client.c
广播接收:socket-->bind-->recvfrom(广播地址处接收数据包) udp_server.c
组播
数据发送方将数据包发送到了某个主机的组播组里,加入 到该组播组的所有的主机能够接收到组播数据
组播地址:224.0.0.1~239.255.255.254
组播发送:socket-->指定目标地址为组播地址-->sendto udp_client.c
组播接收:socket-->加入组播组-->bind->recvfrom udp_server.c
加入组播组:
struct ip_ mreq mreq;-->组播地址
unix域套接字
unix域:socket文件(使用本地协议创建的socket文件)用socket函数使用本地协议可以创建一个socket文件,通过该文件能够实现一台主机上两个进程的通信。
socket(PF_ UNIX)-->socket文件
unix域套接字有两种:流式套接字 数据报套接字
TCP流式套接字
使用TCP协议的网络编程框架创建流式套接字文件,该套接字文件可以实现一台主机上两个本地进程的通信。
服务端:socket-->bind-->listen-->accept-->IO函数
客户端:socket-->connect-->IO函数
注意:tcp方式的unix域套接字,类似无名管道,实现双向通信只需要一个文件。可以实现两个不相关进程的通信
UDP数据报套接字
使用UDP协议的网络编程框架创建数据报套接字文件,该套接字文件可以实现一台主机上两个本地进程的通信。
发送方:socket-->bind-->IO函数
接收方:socket-->bind-->IO函数
注意:udp方式unix域套接字,类似于有名管道,实现双工通信需要两个管道文件。
数据库
数据库:是指以同一组织方式将相关的数据组织在一起,并存放在计算机的存储器上的能够为多个用户所共享的文件,该文件与应用程序彼此独立。
在嵌入式领域常见的数据库有:SQLite mySql-->SQL语句
SQlite是一个开源的,内嵌的关系型数据库。
特点:源码开放 代码精简 免安装 支持SQL语句-->2w+
SQL语句--->用做操作数据库的指令
primary key:-->主键约束
A 主键值必须是唯一的,用于标识每条记录,如学生 的学号
B 主键同时也是一个索引,通过主键查找记录的速度 最快
C 主键如果是整数类型,该列的值可以自动增长。
创建一张表:
create table 表名(字段&约束)
给表插入一条表项:
insert into 表名(字段列表) values (插入值列表)
查询:
select *from 表名-->查整张表
select *from 表名 where 条件1 and/or 条件2
修改记录:
update 表名 set 字段名=新值 where id=2
删除表项:
delete from 表名 where id=1-->条件删除
drop table 表名-->删除整张表