6 网络
1、概念
计算机网络是实现资源共享和信息传递的计算机系统
ISO/OSI网络协议模型
TCP/IP协议
·应用程序负责组织的通常都是与业务相关的数据内容,而要想把这些数据内容通过网络发送出去,就要将其自上向下地压入协议栈,每经历一个协议层,就会对数据做一层封包,每一层输出的封包都是下一层输入的内容,消息包沿着协议栈的运动形成了消息流。
·当从网络上接收数据时,过程刚好相反,消息包自下向上地流经协议栈,每经历一个协议层,就会对输入的数据解一层封包,经过层层解包以后,应用程序最终得到的将只是与业务相关的数据内容
2 IP地址
- 什么是P地址?
IP地址,是IP协议提供的一种统一的地址格式,为互联网上的每个网络和每台主机分配一个逻辑地址,借以消除物理地址所带来的差异性 - IP地址如何表示?
在计算机内部,IP地址用一个32位无符号整数表示,如:0x01020304。
人们更习惯使用点分十进制字符串表示,如:1.2.3.4。字符串形式的从左到右,对应整数形式的从高字节到低字节。注意这里所说的高低指的是数位高低而非地址高低 - 什么是IP地址分级?
A级地址:以0为首的8位网络地址+24位本地地址
B级地址:以10为首的16位网络地址+16位本地地址
C级地址:以110为首的24位网络地址+8位本地地址
D级地址:以1110为首的32位多播地址
例如:某台计算机的IP地址:192.168.182.48,写成整数形式:11000000 10101000 10110110 00110000
代表的是C级地址,网络地址:192.168.182.0,本地地址:48 - 子网掩码
- 借助子网掩码可以快速帮助我们区定IP地址的网络地址和本地地址
- 以IP地址:192.168.182.48,子网掩码:255.255.255.0为例
网络地址=IP地址&子网掩码
192.168.182.48 & 255.255.255.0 = 192.168.182.0
本地地址=IP地址&~子网掩码
192.168.182.48 & 0.0.0.255 = 0.0.0.48
- 以IP地址:192.168.182.48,子网掩码:255.255.255.0为例
- 借助子网掩码可以快速帮助我们区定IP地址的网络地址和本地地址
3、套接字
套接字代表着主机的通信能力
套接字接口库规定在网络传输过程中采用网络字节序,也就是大端字节序,
而本机数据可能是小短字节序
- 小端字节序:数据的低位存放在低地址
- 大端字节序:数据的低位存放在高地址
4、TCP协议
TCP提供客户机与服务器的连接
4.1 TCP协议的基本特征
一个完整TCP通信过程需要依次经历三个阶段
- 首先,客户机必须建立与服务器的连接
- 然后,凭借已建立好的连接,通信双方相互交换数据
- 最后,客户机与服务器双双终止连接,结束通信过程
TCP保证数据传输的可靠性 - TCP的协议栈底层在向另一端发送数据时,会要求对方在一个给定的时间窗口内返回确认。如果超过了这个时间窗口仍没有收到确认,则TCP会重传数据并等待更长的时间。只有在数次重传均告失败以后,TCP才会最终放弃。TCP含有用于动态估算数据往返时间(Round-Trip Time,RTT)的算法,因此它知道等待一个确认需要多长时间
TCP保证数据传输的有序性 - TCP的协议栈底层在向另一端发送数据时,会为所发送数据的每个字节指定一个序列号。即使这些数据字节没有能够按照发送时的顺序到达接收方,接收方的TCP也可以根据它们的序列号重新排序,再把最后的结果交给应用程序
TCP是全双工的 - 在给定的连接上,应用程序在任何时候都既可以发送数据也可以接收数据。因此,TCP必须跟踪每个方向上数据流的状态信息,如序列号和通告窗口的大小
4.2 建立连接
三路握手
- 客户机的TCP协议栈向服务器发送一个SYN分节,告知对方自己将在连接中发送数据的初始序列号
- 服务器的TCP协议栈向客户机发送一个单个分节,其中不仅包括对客户机SYN分节的ACK应答,还包含服务器自己的SYN分节,以告知对方自己在同一连接中发送数据的初始序列号
- 客户机的TCP协议栈向服务返回ACK应答,以表示对服务器所发SYN的确认
tcp包头结构,有20个字节,其中2个字节的源端口、2个字节的目的端口、4字节的序号、4字节的确认号等等共计20个字节的数据
三次握手的解释:
① 客户机首先向服务器发送数据包,数据包中的SYN的比特位是1,并且在序号位置指定一个数字,假定为100
② 服务器收到客户机发来的数据包,其中SYN的比特位是1,那么服务器就会应答一个数据包,数据包里的ACK比特位置1,代表一种应答,并且在确认号这里会将收到的序号+1,这里是101,代表服务器真的收到了客户机的数据包,然后将序号指定一个数字假定是200,并且将SYN比特位置1,一起发送给客户机
③ 客户机收到服务器发来的数据包,首先检查数据包中的ACK和确认号是否正确,然后会向服务器应答一个数据包,其中ACK比特位置1,代表是对服务器的应答,确认号填写收到的序号+1,这里是201,发送给服务器,代表客户机真的收到了服务器的数据包
④ 服务器收到数据包,检查ACK和确认号,至此连接建立
接口## 4.3 交换数据
- 一旦连接建立,客户机即可构造请求包并发往服务器,服务器接收并处理来自客户机的请求包,构造响应包
- 服务器向客户机发送响应包,同时捎带对客户机请求包的ACK应答。
- 客户机接收来自服务器的响应包,同时向对方发送ACK应答
4.4 终止连接
四次挥手
- 客户机或者服务器主动关闭连接,TCP协议栈向对方发送FIN分节,表示数据通信结束。如果此时尚有数据滞留于发送缓冲区中,则FIN分节跟在所有未发送数据之后
- 接收到FIN分节的另一端执行被动关闭,-方面通过TCP协议栈向对方发送ACK应答,另一方面向应用程序传递文件结束符
- 一段时间以后,方才接收到FIN分节的进程关闭自己的连接,同时通过TCP协议栈向对方发送FIN分节
- 对方在收到FIN分节后发送ACK应答
四次挥手的解释
① 客户机关闭套接字之后,客户机首先向服务器发送一个数据包,其中FIN比特位置1
② 服务器收到客户机发来的数据包,其中FIN比特位为1,向客户机回传一个应答,其中ACK置1
③ 服务器关闭对应的套接字操作之后,服务器向客户机发送一个数据包,其中FIN比特位置1
④ 客户机收到服务器发来的数据包,其中的FIN比特位为1,向服务器回传一个应答,其中ACK置1
⑤ 至此连接断开,通信终止
4.5 编程模型
- 相关函数
1:socket 创建套接字
// 头文件 sys/socket.h
int socket(int domain,int type,int protocol);
- 功能:创建套接字
- 参数:
- domain:通信域,协议族,可取以下值:
PF_LOCAL/PF_UNIX - 本地套接字,进程间通信
PF_INET - 基于IPV4的网络通信
PF_INET6 - 基于IPv6的网络通信
PF_PACKET - 基于底层包的网络通信
- type:套接字类型,可取以下值:
SOCK_STREAM - 流式套接字,基于TCP协议
SOCK_DGRAM - 数据报套接字,基于UDP协议
SOCK_RAW - 原始套接字,工作在传输层以下
- protocol:特殊协议,对于流式和数据报套接字而言,只能取0
- 返回值:成功返回表示套接字对象的文件描述符,失败返回-1。
2:相关结构体
/*套接字接口库通过地址结构定位一个通信主体,可以是一个文件,可以是一台远程主机,也可以是执行者自己 */
// 基本地址结构,本身没有实际意义,仅用于泛型化参数
struct sockaddr{
sa_family_t sa_family; // 地址族
char sa_data\[14\]; // 地址值
}
// 本地地址结构,用于AF_LOCAL/AF_UNIX域的本地通信
struct sockaddr_un{
sa_family_t sun_family;// 地址族(AF_LOCAL/AF_UNIX)
char sun_path\[\];// 本地套接字文件的路径
}
// 网络地址结构,用于AF_INET域的IPV4写径通信
struct sockaddr_in{
sa_family_t sin_family;∥地址族(AF_INET)
in_port_t sin_port; //端口号(0~65535) - unsigned short
struct in_addr sin_addr;//IP地址 - unsigned int
}
// 网络地址结构,用于AF_INET域的IPV4网络通信
struct in_addr{
in_addr_t s_addr;
}
typedef uint16_t in_port_t; //无符号16位整数
typedef uint32_t in_addr_t; //无符号32位整数
3:字节序转换函数
uint32_t htonl(uint32_t hostlong); //长整形主机字节序到网络字节序
uint32_t ntohl(uint32_t netllong); //长整形网络字节序到主机字节序
uint16_t htons(uint16_t hostshort); //短整形主机字节序到网络字节序
uint16_t ntohs(uint16_t netshort); //短整型网络字节序到主机字节序
in_addr_t inet_addr(char const* ip); // 点分十进制字符串地址 -> 网络字节序形式整数地址
int inet_aton(char const* ip,struct in_addr* nip); //点分十进制字符串地址 -> 网络字节序形式整数地址
char* inet_ntoa(struct in_addr nip); //网络字节序形式整数地址 -> 点分十进制字符串地址
4:bind 将套接字和本机的地址结构绑定在一起
// 头文件 sys/socket.h
int bind(int sockfd,struct sockaddr const* addr,socklen_t addrlen);
- 功能:将套接字和本机的地址结构绑定在一起
- 参数:
- sockfd:套接字描述符。
- addr:自己的地址结构。
- addrlen:地址结构的字节数
- 返回值:成功返回0,失败返回-1。
5:listen 启动侦听
// 头文件 sys/socket.h
int listen(int sockfd,int backlog)
- 功能:启动侦听:在指定套接字上启动对连接请求的侦听功能
- 参数:
- sockfd:套接字描述符,在调用此函数之前是一个主动套接字,是不能感知连接请求的,在调用此函数并成功返回之后,是一个被动套接字,具有感知连接请求的能力。
- backlog:未决连接请求队列的最大长度,一般取不小于1024的值。
- 返回值:成功返回0,失败返回-1。
6:accept
// 头文件 sys/socket.h
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
- 功能:等待并接受连接请求,在指定套接字上阻塞,直到连接建立完成。
- 参数:
- sockfd:侦听套接字描述符
- addr:输出连接请求发起方的地址信息
- addrlen:输出连接请求发起方的地址信息字节数
- 返回值:成功返回可用于后续通信的连接套接字描述符,失败返回-1。
- 案例
// tcp 服务器
#include <stdio.h>
#include <ctype.h> // toupper
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/socket.h> // 网络相关
#include <sys/types.h>// 网络相关
#include <arpa/inet.h>// 网络相关
#include <sys/wait.h>
// 收尸
void sigfun(int signum){
printf("服务器:收尸\n");
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("没有子进程了\n");
break;
}else{
perror("waitpid");
return ;
}
}else if(pid == 0){
printf("%d进程在运行\n",getpid());
break;
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
}
int main(){
printf("服务器设置信号量\n");
if(signal(SIGCHLD,sigfun)==SIG_ERR){ // 对17号信号做捕获处理
perror("signal");
return -1;
}
printf("服务器:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("服务器:组织地址结构\n");
struct sockaddr_in ser;// IPV4的网络地址结构
ser.sin_family = AF_INET;
ser.sin_port = htons(8980);// 端口号,这里需要做字节序转换,因为TCP协议栈需要这个端口
ser.sin_addr.s_addr = inet_addr("192.168.174.152");
printf("服务器:绑定套接字和地址结构\n");
if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("bind");
return -1;
}
printf("服务器:启动侦听\n");
if(listen(sockfd,1024)==-1){// 监听,用于接收客户端的连接请求(3次握手)
perror("listen");
return -1;
}
for(;;){
printf("服务器:等待连接\n");
struct sockaddr_in cli; //用于输出客户端的地址结构
socklen_t len = sizeof(cli); // 用来输出地址结构的大小
int conn = accept(sockfd,(struct sockaddr*)&cli,&len); // 用于完成与客户端的3次握手中的后两次
if(conn == -1){ // conn 是用来和客户端通信用的,每接入一个客户端,就会生成一个新的conn
perror("accept");
return -1;
}
printf("服务器:接收到%s:%hu的客户端的连接\n",inet_ntoa(cli.sin_addr),ntohs(cli.sin_port));
printf("服务器:业务处理\n");
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
close(sockfd);// 关掉从父进程复制过来的通信套接字
for(;;){
// 接收客户端发送的小写的串
char buf[64]={};
ssize_t size = read(conn,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
close(conn);
return -1;
}
//客户端断开连接时
if(size == 0){break;}
// 转大写
for(int i=0;i<strlen(buf);i++){
buf[i]=toupper(buf[i]);
}
// 将大写的串回传给客户端
if(write(conn,buf,strlen(buf))==-1){
perror("write");
close(conn);
return -1;
}
}
printf("服务器:关闭通信套接字\n");
close(conn);
return 0;
}
close(conn);// 子进程已经复制了这个套接字,父进程自己的直接关掉即可
}
printf("关闭服务器\n");
close(sockfd);
return 0;
}
7:connect 将套接字和对方的地址结构连接在一起
// 头文件 sys/socket.h
int connect(int sockfd,struct sockaddr const* addr,socklen_t addrlen);
- 功能:将套接字和对方的地址结构连接在一起
- 参数:
- sockfd:套接字描述符
- addr:对方的地址结构
- addrlen:地址结构的字节数
- 返回值:成功返回0,失败返回-1
8:send 发送数据
// 头文件 sys/socket.h
ssize_t send(int sockfd,void const* buf,size_t count,int flags);
- 功能:发送数据
- 参数:
- 若flags取0则与write函数完全等价,另外也可取以下值:
MSG_DONTWAIT - 以非阻塞方式接收数据。
MSG_OOB - 接收带外数据。
MSG_DONTROUTE - 不查路由表,直接在本地网络中寻找目的主机
- 返回值:成功返回实际发送的字节数,失败返回-1。
9:recv 接收数据
// 头文件 sys/socket.h
ssize_t recv(int sockfd,void* buf,size_t count,int flags);
- 功能:接收数据
- 参数:
- 若flags取0则与read函数完全等价,另外也可取以下值:
MSG_DONTWAIT - 以非阻塞方式接收数据。
MSG_OOB - 接收带外数据。
MSG_WAITALL - 等待所有数据,即不接收到count字节就不返回。
- 返回值:成功返回实际接收到的字节数,失败返回-1。
- 案例
// 基于tcp客户端的连接
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(){
printf("客户端:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("客户端:组织服务器的地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(8980);
ser.sin_addr.s_addr = inet_addr("192.168.174.152");
printf("客户端:发起连接\n");
if(connect(sockfd,(struct sockaddr*)&ser,sizeof(ser))==-1){
perror("connect");
return -1;
}
printf("客户端:业务处理\n");
for(;;){
// 发送小写的串
char buf[64]={};
fgets(buf,sizeof(buf),stdin);
if(strcmp(buf,"!\n")==0){
break;
}
if(send(sockfd,buf,strlen(buf),0)==-1){
perror("send");
return -1;
}
// 接收大写的串
if(recv(sockfd,buf,sizeof(buf)-1,0)==-1){
perror("recv");
return -1;
}
// 显示
printf("%s",buf);
}
printf("客户端:关闭套接字\n");
close(sockfd);
return 0;
}
5、UDP协议
5.1 UDP协议的基本特性
- UDP不提供客户机与服务器的连接
- UDP的客户机与服务器不必存在长期关系。一个UDP的客户机在通过一个套接字向一个UDP服务器发送了一个数据报之后,马上可以通过同一个套接字向另一个UDP服务器发送另一个数据报。同样,一个UDP服务器也可以通过同一个套接字接收来自不同客户机的数据报
- UDP不保证数据传输的可靠性和有序性
- UDP的协议栈底层不提供诸如确认、超时重传、RTT估算以及序列号等机制。因此UDP数据报在网络传输的过程中,可能丢失,也可能重复,甚至重新排序。应用程序必须自己处理这些情况
- UDP不提供流量控制
- UDP的协议栈底层只是一味地按照发送方的速率发送数据,全然不顾接收方的缓冲区是否装得下
- UDP是全双工的
- 在一个UDP套接字上,应用程序在任何时候都既可以发送数据也可以接收数据
5.2 常用函数
1:recvfrom 从哪里接收数据
// 头文件 sys/socket.h
ssize_t recvfrom(int sockfd,void* buf,size_t count,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
- 功能:从哪里接收数据
- 参数:
- 前四个参数和函数recv相同
- src_addr:输出源主机的地址信息
- addrlen:输入输出源主机的地址信息的字节数。
- 返回值:成功返回实际接收的字节数,失败返回-1
2:sendto 发送数据到哪里
// 头文件 sys/socket.h
ssize_t sendto(int sockfd,void const* buf,size_t count,int flags,struct sockaddr const* dest_addr,socklen_t addrlen);
- 功能:发送数据到哪里
- 参数:
- 前四个参数和函数send相同
- dest_addr:目的主机的地址信息。
- addrlen:目的主机的地址信息的字节数。
- 返回值:成功返回实际发送的字节数,失败返回-1
5.3 UDP通信模型
- 服务器
// udp 服务器
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys//types.h>
#include <arpa/inet.h>
int main(){
printf("服务器:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("服务器:组织地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(8090);
ser.sin_addr.s_addr=inet_addr("192.168.174.152");
printf("服务器:绑定套接字和地址结构\n");
if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser))==-1){
perror("bind");
return -1;
}
printf("服务器:业务处理\n");
for(;;){
char buf[64]={};
struct sockaddr_in cli;
socklen_t len = sizeof(cli);
if(recvfrom(sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&cli,&len)==-1){
perror("recvfrom");
return -1;
}
// 转大写
for(int i=0;i<strlen(buf);i++){
buf[i] = toupper(buf[i]);
}
// 将大写的串回传到客户端
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*) &cli,len) == -1){
perror("sendto");
return -1;
}
}
printf("服务器关闭套接字\n");
close(sockfd);
return 0;
}
- 客户端
// UDP客户端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(){
printf("客户端:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socker");
return -1;
}
printf("客户端:组织服务器地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(8090);
ser.sin_addr.s_addr = inet_addr("192.168.174.152");
printf("客户端:业务处理\n");
for(;;){
// 向客户端发送小写的串
char buf[64] = {};
fgets(buf,sizeof(buf),stdin);
if(strcmp(buf,"!\n")==0){
break;
}
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&ser,sizeof(ser))==-1){
perror("sendto");
return -1;
}
// 接收服务器回传
if(recv(sockfd,buf,sizeof(buf)-1,0)==-1){
perror("recv");
return -1;
}
// 显示
printf("客户端:收到%s",buf);
}
// 关闭套接字
close(sockfd);
return 0;
}
6、域名解析
- IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替1P地址标识站点地址
- 域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成
- 当应用过程需要将一个主机域名映P地址时,就调用域名解析函数,解析函数将待转换的域名放在DNS请求中,以UDP报文方式发给本地域名服务器。本地的域名服务器查到域名后,将对应的IP地址放在应答报文中返回。
相关函数
gethostbyname 获取主机信息
// 头文件 netdb.h
struct hostent* gethostbyname(char const* host_name);
- 功能:通过参数所传的主机域名,获取主机信息
- 参数:
- host_name 主机域名
- 返回值:函数执行成功返回表示主机信息的结构体指针,失败返回NULL
- 注意,该函数需要再联网情况下使用
结构
struct hostent{
char *h_name; //主机官方名
char **h_aliases; //主机别名表
int h_addrtype; //地址类型
int h_length; //地址长度
char **h_addr_list; //IP地址表
};
- 案例
#include <stdio.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
int main(int argc,char* argv[]){
struct hostent* h = gethostbyname(argv[1]);
if(h==NULL){
perror("gethostbyname");
return -1;
}
printf("主机官方名\n");
printf("\t%s\n",h->h_name);
printf("主机别名\n");
for(char **pp = h->h_aliases;*pp;pp++){
printf("\t%s\n",*pp);
}
printf("IP地址表\n");
for(struct in_addr **pp=(struct in_addr**)h->h_addr_list;*pp;pp++){
printf("\t%s\n",inet_ntoa(**pp));
}
return 0;
}
- 创建http请求发送
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(){
// 创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
// 组织百度服务器的地址结构
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(80);
ser.sin_addr.s_addr = inet_addr("36.155.132.76");
// 向百度服务器发起连接
if(connect(sockfd,(struct sockaddr*)&ser,sizeof(ser))==-1){
perror("connect");
return -1;
}
// 组织http请求并发送给百度服务器
char request[1024] ={};
sprintf(request,"GET / HTTP/1.1\r\n"
"Host: www.baidu.com\r\n"
"Accept: */*\r\n"
"Connection: close\r\n\r\n");
if(send(sockfd,request,strlen(request),0)==-1){
perror("send");
return -1;
}
// 接收百度服务器回传的响应
for(;;){
char respond[1024] = {};
ssize_t size = recv(sockfd,respond,sizeof(respond)-1,0);
if(size == -1){
perror("recv");
return -1;
}
if(size == 0){
break;
}
printf("%s",respond);
}
printf("\n");
close(sockfd);
return 0;
}