广播(使用 UDP 套接字)
广播地址:主机号最大的地址。
广播:给所在局域网的所有主机发送数据报。(之前的数据报发送方式是单播。)
以下情况中使用广播: 局域网 搜索协议。
比如家中的智能产品, 使用手机可以搜索出附近的智能产品,这就是一个局域网搜索协议。
基于 setsockopt 实现广播
广播发送者(客户端):
1、创建一个数据报套接字;
int sockfd = sock(AF_INET, SOCK_DGRAM, 0);
2、setsockopt(sockfd, 协议层, 选项名, 数据类型, 大小);
int opt = 1; // 非0即可
setsockopt(sockfd, **SOL_SOCKET**, SO_BROADCAST, &opt, sizeof(op));
3、填充结构体;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr,sin_port = htons(atoi(argv[2]));
4、发送数据报;
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&addr, sizeof(addr));
广播接收者(服务器):
1、创建一个数据报套接字;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
2、绑定广播IP;
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 广播地址 或 0.0.0.0
socklen_t length = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){...}
3、等待接收数据;
recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &length);
缺点:
广播发送给所有主机,过多的的网络会发送大量的网络带宽,造成广播风暴。
广播风暴: 网络长时间被大量数据包占用,无法通信,网络会变得缓慢,甚至崩溃。
机制: 通过向广播地址发送UDP数据包,将数据包发送给网络中的所有主机。当一个主机发送广播消息时,该消息会被路由器转发到网络中的所有子网。然后,每个子网上的主机都会接收到该广播消息,并转发给它们的相邻主机。这个过程会一直持续下去,直到广播消息传播到整个网络中的所有主机,如果这个现象一直循环如此,则会造成广播风暴。
广播:给发送者设定相应的权限;
组播:接收者要加入到多播组;
组播(使用 UDP 套接字)
组播地址(D类IP):224.0.0.1 ~ 239.255.255.255
基于 setsockopt 实现组播
// 多播结构体
struct ip_mreq{
struct in_addr imr_multiaddr; // 指定多播组IP
struct in_addr imr_interface; // 本地IP,通常指定为 INADDR_ANY--0.0.0.0
}
struct in_addr{
_be32 s_addr; // IP地址(大端)
}
组播接收者(服务器):
1、创建数据报套接字;
int sockfd = sock(AF_INET, SOCK_DGRAM, 0);
2、加入多播组(仅限于接收者);
// 核心代码 ------------------------------------
struct ip_mreq mreq; // 定义组播的结构体变量
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(argv[1]); // 填充多播组IP
mreq.imr_interface.s_addr = inet_addr("0.0.0.0"); // 自动获取本机IP
// 改变套接字属性
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
3、填充结构体,绑定 组播IP 和 端口;
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 组IP
socklen_t length = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){...}
4、等待接收数据;
recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &length);
组播发送者(客户端):
1、创建数据报套接字
2、指定接收方地址为 组播地址,设置端口信息
3、发送数据报
广播和组播的区别:
● 广播方式:将数据报发给所有的主机。过多的广播会占用大量网络带宽,造成广播风暴,影响通信。
● 组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。
● 组播方式既可以发给多个主机,又能避免像广播那样带来过多的负载。
本地套接字通信
· unix 网络编程:最开始都是一台主机内进程和进程之间的编程。(本地通信)
· socket:可以用于本地间进程通信,创建套接字时使用本地协议 AF_LOCAL 或 AF_UNIX。
特点:
本地通信不需要IP和端口,无法进行两个主机通信;
分为流式套接字和数据报套接字; // 可以使用 流式套接字 或者 数据包套接字
和其他进程间通信相比,使用方便、效率更高,常用于前、后台进程通信;
unix 域套接字编程,实现本间进程的通信,依赖的是 s 类型的文件;
核心步骤:
#include <sys/socket.h>
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; /* 本地协议 AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 本地路径 s类型的套接字文件 */
};
unix socket = socket(AF_UNIX, type, 0); // type 可以为流式套接字或数据包套接字
// unix 写为 int 就可以
struct sockaddr_un myaddr;
myaddr.sun_family = AF_UNIX; // 填充 UNIX 域套接字
strcpy(saddr.sun_path,"./myunix"); // 创建套接字的路径
相关代码
receiver
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("sock is err:");
return -1;
}
system("rm ./myunix -f");
unlink("./myunix");
// 2. 填充结构体
struct sockaddr_un saddr;
saddr.sun_family = AF_UNIX;
strcpy(saddr.sun_path, "./myunix");
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("bind is err:");
return -1;
}
if (listen(sockfd,5) < 0){
perror("listen is err:");
return -1;
}
int acceptfd = accept(sockfd, NULL, NULL);
if (acceptfd < 0){
perror("acceptfd < 0");
return -1;
}
char buf[128] = "";
int recvbyte;
while(1){
recvbyte = recv(acceptfd, buf, sizeof(buf), 0);
if (recvbyte < 0){
perror("recv is err:");
return -1;
}
else if (recvbyte == 0){
printf("exit\n");
break;
}
else{
printf("buf: %s\n", buf);
}
}
close(sockfd);
close(acceptfd);
return 0;
}
sender
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <poll.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0){
perror("sock is err:");
return -1;
}
// 2. 填充结构体
struct sockaddr_un saddr;
saddr.sun_family = AF_UNIX;
strcpy(saddr.sun_path, "./myunix");
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("connect is err:");
return -1;
}
char buf[128] = "";
int recvbyte;
while(1){
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf) -1] == '\n')
buf[strlen(buf) -1] = '\0';
send(sockfd, buf, sizeof(buf), 0);
}
close(sockfd);
return 0;
}
网络头协议分析
wireshark抓包工具
抓包工具的使用
虚拟机: sudo apt-get install wireshark
windows: 小飞机
(抓包的过程,就是抓取流经网卡的数据。如果不加 sudo 就找不到网卡,没有办法抓到数据。)
两台不同的主机通信 或 两台不同的操作系统(windows、linux)之间 才可以进行抓包。
步骤
1)运行 linux 下的服务器;
2)打开 windows 下的小飞机;
3)打开抓包工具;
4)过滤无关的包;
5)小飞机模拟客户端, 与 linux 下的服务器通信;
实现:
包头分析
以太网的完整帧格式
网络层最大数据帧长度是1500字节。(MTU: 最大传输单元)
链路层最大数据长度是1518字节。(网络层 1500 + 以太网 14 + CRC检错 4)
TCP 粘包、拆包 与 UDP丢包
● 发生 TCP 粘包或拆包 有很多原因,常见的有以下几点:
1、待发送数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
3、待发送数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
● TCP 粘包的 解决办法:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补 \0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
以太网头
以太网中封装了 目的mac地址 以及 源mac地址、IP 类型,以太网头又称为 mac头。
切换网络时,IP地址会改变,Mac地址不会改变。
type 类型:
0x0800 ——> 只接收发往 本机MAC 的 IP类型的数据帧;
0x0806 ——> 只接收发往 本机 ARP类型 的数据帧;
0x8035 ——> 只接收发往 本机 RARP类型 的数据帧;
0x0003 ——> 接收发往 本机MAC 的所有类型:IP, ARP, RARP 数据帧,接收从本机发出去的数据帧,
当混杂模式打开的情况下,会接收到非发往本地的 MAC 数据帧。
ARP:ARP协议用于将 IP地址 解析为 MAC地址。当一台计算机向另一台计算机发送数据时,它需要知道目标计算机的 MAC地址,而不是 IP地址。
RARP:RARP协议则是与 ARP 相反的过程,它用于将 MAC地址 解析为 IP地址。
IP头
UDP头
TCP头
三次握手
服务器必须准备好接受外来的连接。这通过调用 socket、 bind 和 listen 函数来完成,称为被动打开(passive open)。
第一次握手:客户通过调用 connect 进行主动打开(active open)。客户端发送一个SYN(表示同步)
分节(SYN=J),它告诉服务器客户将在连接中发送到数据的初始序列号。并进入 SYN_SEND 状态,
等待服务器的确认。
第二次握手:服务器必须确认客户的 SYN,同时自己也得发送一个 SYN 分节,它含有服务器将在同一连接
中发送的数据的初始序列号。服务器以单个字节向客户发送 SYN 和 对客户 SYN 的 ACK(表示确认)
此时服务器进入 SYN_RECV 状态。
第三次握手:客户收到服务器的 SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户服务器进入
ESTABLISHED(确认)状态,完成三次握手。
四次挥手
第一次挥手:主动关闭方发送一个 FIN 给被动方,进入 FIN_WAIT 状态;
第二次挥手:被动方接收到 FIN 包,给主动方发送一个 ACK 包;并进入 CLOSE_WAIT 状态,主动方接收
到 ACK 包后,如果有数据没有发送完毕,则继续发送,一直到发送完毕;
第三次挥手:被动方发送一个 FIN 包,进入 LAST_ACK 状态。
第四次挥手:主动关闭方收到 FIN 包,回复一个 ACK包。被动关闭方收到主动关闭方的 ACK后关闭连接。