6.21 静态库与动态库
库有两种:静态库(.a、.lib)和动态库(.so、.dll)。所谓静态、动态是指链接。静态库在链接期把整个库文件都拷贝到可执行文件中,而动态库在链接期只是把索引文件拷贝到可执行文件中,在运行时才调用动态库。(通过索引文件找到动态库文件)
静态库与动态库优缺点
静态库:
优点:由于静态库是将整个库文件都拷贝到可执行文件中,所以调用速度相对快。
缺点:1.浪费内存空间(多个应用程序依赖同一个静态库时,多个程序运行时,会在内存中拷贝多份静态库)2.对程序的更新、部署、发布比较麻烦,静态库修改后,需要重新编译整个可执行文件,重新安装。
动态库:
优点:1.节省内存空间(多个应用程序依赖同一个动态库时,多个程序运行时,只会在内存中拷贝一份动态库)2.对程序的更新、部署、发布简单,动态库修改后,只需要重新编译动态库即可,不需要重新编译整个可执行文件。
缺点:调用速度比静态库慢,移植的时候需要同时移植可执行文件和动态库。
6.22 网络基本模型
七层网络模型
应用层
用来提供应用程序之间的信息通信的(例:淘宝、qq、微信等)
涉及到的协议(protocol):
HTTP
超文本传输协议,TCP端口 80
HTTPS
超文本传输安全协议,TCP端口 443,相比HTTP多了SSL加密方式,更安全
FTP
File Transfer Protocol,文件传输协议,TCP端口 23
DNS
Domain Name System,域名系统,TCP/UDP端口 53,将域名解析成IP地址
DHCP
Dynamic Host Configuration Protocol,动态主机配置协议,UDP端口 68,动态获取网络配置
Telnet
远程终端协议,TCP端口 23,远程控制Web服务器
SMTP
简单邮件传输协议,TCP端口 25,提供可靠且有效的电子邮件传输的协议
SSH
Secure Shell,安全外壳,是一种网络安全协议,通过加密和认证机制实现安全的访问和文件传输等业务
表示层
提供数据格式转换的,主要用来数据加解密、数据解压缩。图片/视频编解码。
几个注意点:
数据在发送之前要用一定的加密方法进行加密
想要使数据发送的快,就要对数据进行压缩,这样也会使网络更通畅
实时传输(直播)一般都需要视频编解码,不然延迟会高
会话层
session会话管理、服务器验证用户登录、断点续传。
session会话管理 例:如果我们在登录一个网站之后,浏览页面过程中要跳转网页,那么不需要重复登录,靠的就是session会话管理,它保证了用户在同一个网站中不管多少次的页面跳转,都不需要重新登录
断点续传 例:在下载文件中途有网络波动,在一段时间内网络恢复了,那么会接着下载
传输层
(操作系统级别的)
TCP
TCP,传输控制协议,提供一种面向连接的、可靠的、基于字节流的传输层通信协议,有流量控制和差错控制,使用TCP协议的应用比如邮件的接收和发送、文件传输、远端登录。需要数据稳定和完整性比较高的场景多使用TCP协议。
UDP
UDP,用户数据报协议,提供一种无连接的、高效率、低可靠性的数据传输服务,使用UDP协议的应用比如音视频聊天、在线游戏王者荣耀等、工业物联网数据传输等。需要数据时效性比较高的场景多使用UDP协议。
线程
端口
注:一个应用程序可以使用多个端口,但是一个端口只能被一个应用程序使用
socket(套接字)
是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
其中TCP/IP协议不是TCP和IP协议,而是代表一个协议族
数据传输一定需要的两个信息:
IP地址和端口号,IP地址能够保证数据到达目标设备,端口号保证数据能从设备到应用程序
网络层
网络层不止有协议,还有实际的设备,比如路由器
协议有IP、ARP、RARP、防火墙
网络层主要做的是寻址和路由选择
可以通过ipconfig查看自己的ip地址
数据链路层
交换机、网卡(MAC地址)
设备有几个网卡,就有几个ip地址
交换机只能进行单纯的转发,大家要连到同一台交换机上,而路由器除了支持数据转发外还支持路由寻址
物理层
光纤、网线
把数字信号转换为电信号或光信号进行比特流传输
TCP/IP事实标准网络模型
包含应用层、传输层、网络层、物理层
C/S架构与B/S架构对比
C/S(Client/Server)
客户端和服务端可以使用任意协议(常用TCP、UDP)连接
需要指定客户端,每个应用程序都有指定的客户端
B/S(Browser/Server)
客户端不受限制
浏览器和服务端必须使用 http 或 https 协议
6.23 实战训练:C/S架构
基于UDP
![](https://img-blog.csdnimg.cn/direct/44679b6b1bcd4cbfba5325f296a6e16c.png)
服务端代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
using namespace std;
#include<WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//加载库
WORD wVersion = MAKEWORD(2, 2);/*创建版本号 2.2 版本*/
WSAData data;
int err = WSAStartup(wVersion,&data);
if (0 != err)
{
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
if (2 != LOBYTE(data.wVersion) || 2 != HIBYTE(data.wVersion))/*判断加载的版本号是否正确*/
{
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
//创建套接字
SOCKET sock = socket(AF_INET/*IPV4版本*/, SOCK_DGRAM/*UDP报文段*/, IPPROTO_UDP/*协议类型为UDP*/);
if (INVALID_SOCKET == sock)
{
printf("sock failed with error: %d\n",WSAGetLastError());
WSACleanup();
return 1;
}
else
printf("sock success\n");
//绑定ip和端口号
/*这里相当于初始化结构体*/
struct sockaddr_in addrServer;
addrServer.sin_family = AF_INET;/*IPV4类型*/
addrServer.sin_addr.S_un.S_addr = INADDR_ANY;/*任意网卡*/
addrServer.sin_port = htons(12345);/*htons 转换成网络字节序*/
/*这里是绑定*/
err = bind(sock,(sockaddr*)&addrServer,sizeof(addrServer));
if (SOCKET_ERROR == err)
{
printf("bind failed with error: %d\n", WSAGetLastError());
//关闭套接字
closesocket(sock);
//卸载库
WSACleanup();
return 1;
}
else
printf("bind success\n");
//收发数据
int recvNum = 0;
char recvBuf[1024] = "";
int sendNum = 0;
char sendBuf[1024] = "";
struct sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (true)
{
//接收消息
recvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, (sockaddr*)&addrClient, &addrClientSize);
if (recvNum > 0)
{
//打印收到的数据
char ip[100] = "";
cout << "ip:" << inet_ntoa(addrClient.sin_addr) << ":" << recvBuf << endl;
/*inet_addr 将点分十进制ip地址转换为ulong类型,inet_ntoa 将ulong类型转换为点分十进制*/
}
else if (recvNum == 0)
{
cout << "connect closed,please try again..." << endl;
}
else
break;
//发送消息
gets_s(sendBuf);//读取数据
/*sendto 给谁发信息就绑定谁的地址信息*/
sendNum = sendto(sock,sendBuf, sizeof(sendBuf), 0, (sockaddr*)&addrClient, addrClientSize);
if (SOCKET_ERROR == sendNum)
{
cout << "send error" << endl;
break;
}
}
//关闭套接字
closesocket(sock);
//卸载库
WSACleanup();
return 0;
}
客户端代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
using namespace std;
#include<WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//加载库
WORD wVersion = MAKEWORD(2, 2);/*创建版本号 2.2 版本*/
WSAData data;
int err = WSAStartup(wVersion, &data);
if (0 != err)
{
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
if (2 != LOBYTE(data.wVersion) || 2 != HIBYTE(data.wVersion))/*判断加载的版本号是否正确*/
{
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
//创建套接字
SOCKET sock = socket(AF_INET/*IPV4版本*/, SOCK_DGRAM/*UDP报文段*/, IPPROTO_UDP/*协议类型为UDP*/);
if (INVALID_SOCKET == sock)
{
printf("sock failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
else
printf("sock success\n");
//接收端的地址信息
struct sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(12345); //htons转换成网络字节序
addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.246.1");
//收发数据
int recvNum = 0;
char recvBuf[1024] = "";
int sendNum = 0;
char sendBuf[1024] = "";
while (true)
{
//发送消息
gets_s(sendBuf);//读取数据
sendNum = sendto(sock, sendBuf, sizeof(sendBuf), 0, (sockaddr*)&addrServer, sizeof(addrServer));
if (SOCKET_ERROR == sendNum)
{
cout << "send error" << endl;
break;
}
//接收消息
recvNum = recvfrom(sock, recvBuf, sizeof(recvBuf), 0, NULL/*传出参数,后续用不上addrServer了,所以可以传NULL,后面的NULL同理*/, NULL);
if (recvNum > 0)
{
//打印收到的数据
cout << "server say:" << recvBuf << endl;
}
else if (recvNum == 0)
{
cout << "connect closed,please try again..." << endl;
}
else
break;
}
//关闭套接字
closesocket(sock);
//卸载库
WSACleanup();
return 0;
}
运行结果
可见,服务端和客户端可以实现一来一回收发数据。
基于TCP
服务端代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//加载库
WORD wVersion = MAKEWORD(2, 2);
WSAData data;
int err = WSAStartup(wVersion, &data);
if (err != 0)
{
cout << "WSAStartup error" << endl;
return 1;
}
if (2 != LOBYTE(data.wVersion) || 2 != HIBYTE(data.wVersion))
{
cout << "WSAStartup fail" << endl;
WSACleanup();
return 1;
}
else
{
cout << "WSAStartup success" << endl;
}
//创建套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//TCP协议
if (INVALID_SOCKET == sock)
{
cout << "sock error" << endl;
}
else
{
cout << "sock success" << endl;
}
//绑定IP
struct sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(12345);
addrServer.sin_addr.S_un.S_addr = INADDR_ANY;//绑定任意网卡
err = bind(sock, (sockaddr*)&addrServer, sizeof(addrServer));
if (SOCKET_ERROR == err)
{
cout << "bind error" << endl;
}
//监听
err = listen(sock, 10);
if (err == SOCKET_ERROR)
{
cout << "listen error" << endl;
}
else
{
cout << "listen success" << endl;
}
struct sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
//接受连接
SOCKET m_sock = accept(sock, (sockaddr*)&addrClient, &addrClientSize);
if (INVALID_SOCKET == m_sock)
{
cout << "accept fail" << endl;
}
else
{
cout << "accept success" << endl;
}
//收发数据
int sendNum = 0;
char sendBuf[1024] = "";
int recvNum = 0;
char recvBuf[1024] = "";
while (1)
{
//接收数据
recvNum = recv(m_sock, recvBuf, sizeof(recvBuf), 0);
if (recvNum > 0)
{
//打印出客户端的IP地址
char ip[100] = "";
cout << "ip:" << inet_ntoa(addrClient.sin_addr) << ":" << recvBuf << endl;
}
else if (recvNum == 0)
{
cout << "connect closed" << endl;
}
else
{
break;
}
//发送数据
gets_s(sendBuf);
sendNum = send(m_sock, sendBuf, sizeof(sendBuf), 0);
if (SOCKET_ERROR == sendNum)
{
cout << "send error" << WSAGetLastError() << endl;
}
}
closesocket(m_sock);
WSACleanup();
}
客户端代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//加载库
WORD wVersion = MAKEWORD(2, 2);
WSAData data;
int err = WSAStartup(wVersion, &data);
if (0 != err)
{
cout << "WSAStartup error" << endl;
}
if (2 != LOBYTE(data.wVersion) || 2 != HIBYTE(data.wVersion))
{
cout << "WSAStartup fail" << endl;
}
else
{
cout << "WSAStartup success" << endl;
}
//创建套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//TCP协议
if (INVALID_SOCKET == sock)
{
cout << "socket error" << WSAGetLastError() << endl;
}
else
{
cout << "socket success" << endl;
}
//连接
struct sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(12345);
addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.248.1");
err = connect(sock,(sockaddr*)&addrServer,sizeof(addrServer));
if (SOCKET_ERROR == err)
{
cout << "connect error" << endl;
}
else
{
cout << "connect success" << endl;
}
//收发数据
int sendNum = 0;
char sendBuf[1024] = "";
int recvNum = 0;
char recvBuf[1024] = "";
while (1)
{
//发送数据
gets_s(sendBuf);
sendNum = send(sock, sendBuf, sizeof(sendBuf), 0);
if (SOCKET_ERROR == sendNum)
{
cout << "send error:" << WSAGetLastError() << endl;
break;
}
//接收数据
recvNum = recv(sock, recvBuf, sizeof(recvBuf), 0);
if (recvNum > 0)
{
cout << "server say:" << recvBuf << endl;
}
else if (recvNum == 0)
{
cout << "connect closed" << endl;
}
else
{
break;
}
}
closesocket(sock);
WSACleanup();
return 0;
}
运行结果
可见,服务端和客户端可以实现一来一回收发数据。
常见报错原因
无法解析的外部符号:没有添加依赖库
未定义标识符:没有头文件
6.24 ARP协议和DNS协议
数据包在传输过程中的变化
如图所示,发送方的原数据最开始是在应用层,然后在传输层加上UDP头,在网络层加上IP头,在物理层加上帧头、帧尾和CRC校验,然后接收方会逐层校验拆包,直到在应用层得到原数据。
单播、组播和广播
单播就是一对一的,广播就是看目标设备在不在范围内,组播是看目标设备是不是要发送的设备
ARP协议
地址解析协议,是根据IP地址获取MAC地址的一个TCP/IP协议,用在双端设备在通讯收发数据之前,如果不知道对端的MAC地址,就可以通过ARP协议来获取
ARP协议是跨数据链路层和网络层的,所以他的上层协议是IP协议
ARP协议实现的过程就是自己设备会发送一个ARP Request的广播,那么当前路由器所连接的所有设备都会收到这个广播,数据包中的目的MAC填0,所有收到这个广播的设备都会看自己的IP是不是这个广播的目IP,如果不是就会将这个包丢弃,如果是就会回复一个ARP Reply的单播,然后就可以开始通信了
ARP代理
当发送端广播ARP请求时,本地网络上不会有主机回应(因为IP地址是外网的),此时路由器将会回应该请求,则发送源误认为路由器就是目的主机,会将报文全部转发给它,再由路由器转发报文到外网,则该路由器就被称为ARP代理。
免费ARP
在主机开机配置时,会发送一个目的IP地址为自己IP地址的ARP请求报文,该报文称之为免费ARP,其作用如下:
1.让主机确认本地网络上是否有与自己IP地址相同的主机,若有,则会返回一个错误报文。
2.告诉整个广播域,目前某个IP所对应的MAC地址是什么——这一行为就像是在发宣传单,而宣传单是不需要回应的。若接收主机ARP缓存中本身就有发送源主机的IP-MAC对,则会更新,否则,会缓存发送源的IP-MAC对。
路由数据转发过程
原设备和目的设备不是连接同一个路由器上的,需要经过多个路由器转发才能连接上
DNS协议
域名解析协议,DNS服务器将好记的域名解析成IP地址
过程:将客户端要访问的域名传到本地DNS服务器,如果缓存里有就直接返回IP地址,如果没有就访问DNS根服务器,发现是.com域,那么本地DNS服务器再访问.com域服务器,发现是.163.com域,那么再次访问.163.com域服务器寻找IP地址,最后本地DNS服务器返回客户端一个IP地址。
6.25 子网划分
IP地址分类
子网掩码
又叫网络掩码,地址掩码,子网络遮罩。本机IP地址的二进制与子网掩码的二进制按位与可以得到某主机对外IP地址。
默认子网掩码
想判断自己与他人是否在一个子网内,就用自己的IP地址与子网掩码按位与,看与他人是否相同。
非默认子网掩码
非默认子网掩码中1的个数一定比默认子网多,并且1要连续,这些1是从原来主机号中借来的。
网关
广播地址
用于向网络中的所有设备进行广播。具有正常的网络号部分,而主机号部分全为1的IP地址称之为广播地址。
注:主机号全为0的地址为网关地址,主机号全为1的地址为广播地址。
有限广播地址:指的是32位全为1(即255.255.255.255)的IP地址,用于本网广播。
直接广播地址:正常的广播地址称为直接广播地址,发送直接广播地址数据,会将数据发送给该子网内所有主机。
子网划分常见问题
1.选定的子网掩码将创建多少个子网?
2^x个,其中x是子网掩码借用的主机位数。如:192.168.10.32/28,其中28是网络号的意思。我们知道C类IP默认子网掩码为255.255.255.0,C类IP的网络号应该是24位,故其借用了主机位4位来充当网络位,所以创建的子网个数就是2 ^ 4 = 16。
2.每个子网可包含多少台主机?
2^y-2台,其中y是没有被借用的主机位数。-2是因为主机位全为0的部分是这个子网的网关,全为1的部分是这个子网的广播地址。
3.有哪些子网?
想知道有哪些子网,首先要算出子网的步长(增量)。例:256-192=64,即子网掩码为192时,步长为64。从0开始不断增加,直到到达子网掩码值,中间的结果就是子网,即0,64,128,192。
子网划分练习题
7.2 直接广播与有限广播
将上面的代码服务端变成只接收不发送,客户端变成只发送不接收。
直接广播
客户端添加绑定网卡
//3.绑定网卡
struct sockaddr_in addrClient;
addrClient.sin_family = AF_INET;
addrClient.sin_addr.S_un.S_addr = inet_addr("192.168.248.1");
addrClient.sin_port = htons(54321);
err = bind(sock, (sockaddr*)&addrClient, sizeof(addrClient));
if (SOCKET_ERROR == err)
{
printf("bind failed with error: %d\n", WSAGetLastError());
//关闭套接字
closesocket(sock);
//卸载库
WSACleanup();
return 1;
}
else
printf("bind success\n");
同时,接收端地址变为广播地址
//接收端的地址信息
struct sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(12345); //htons转换成网络字节序
addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.248.255");//这里变成广播
这样,就可以发送广播了
有限广播
有限广播的地址是255.255.255.255,但是我们直接添加
addrServer.sin_addr.S_un.S_addr = inet_addr("255.255.255.255");//有限广播
会报错,错误如下:
这是因为有限广播不能直接用,必须申请权限才能使用
addrServer.sin_addr.S_un.S_addr = inet_addr("255.255.255.255");//有限广播
//申请有限广播权限
bool val = true;
setsockopt(sock,SOL_SOCKET,SO_BROADCAST,(char*)&val,sizeof(val));
具体参见: setsockopt function (winsock2.h) - Win32 apps | Microsoft Learn
运行结果
直接广播与有限广播的区别
直接广播可指定任意广播域(限一个),即一个子网。需要计算直接广播地址(主机号全为1),不需要申请广播权限。
有限广播仅限于本网络,即自己在哪个子网,就只能在哪个子网内发送有限广播。不需要计算有限广播地址(255.255.255.255),需要申请广播权限。
7.17 阻塞与非阻塞
阻塞
当一个进程向另一个进程发送请求时,如果对方没有响应,那么当前进程会一直等待,直到对方返回结果或者超时。
非阻塞
当一个进程向另一个进程发送请求时,即使对方没有响应,当前进程也可以继续执行其他任务,不必等待对方的响应。
socket默认阻塞,以下是设置socket为非阻塞:
//设置socket为非阻塞
u_long val = 1;
ioctlsocket(sock,FIONBIO,&val);
参考:
发送的阻塞和非阻塞
发送阻塞
当发送缓冲区空间不足够大的时候,等到发送缓冲区空间足够大再发送。
发送非阻塞
当发送缓冲区空间不足够大的时候,有多少空间就往里拷贝多少内容,剩下的数据应用程序自己处理。
发送缓冲区和接收缓冲区
每创建一个socket,操作系统就会给这个socket分配一个接收缓冲区和发送缓冲区。
以下是查看接收缓冲区和发送缓冲区大小的方法:
int recvSize = 0;
int sendSize = 0;
int size = sizeof(int);
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&recvSize, &size);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sendSize, &size);
cout << "recvSize:" << recvSize << ", sendSize:" << sendSize << endl;
输出结果:
单位为字节,可见发送缓存和接收缓存大小均为64K。