目录
一、前情提要
① 五元组
源端IP
端口号
② 客户端与服务端
客户端:
服务端:
关系:
二、UDP通信的两端流程
对于服务端来说:
1、创建套接字
2、为套接字绑定地址信息
3、接受数据
4、发送数据
5、关闭套接字
三、相关接口使用
1、创建套接字
① int socket(int domain, int type, int protocol);
② domain:表示地址域类型
③ type 套接字类型
④ protocol 协议类型(默认为0,表示使用默认对应协议)
⑤ 返回值 创建成功返回一个套接字描述符,失败返回-1
2、为套接字绑定地址信息
①int bind(int sockfd, struct scokaddr *addr, socklen_t len);
② sockfd: 创建套接字返回的套接字描述符
③ addr 要绑定的地址信息(不同的地址域类型,有不同的地址结构)
④ 成功返回0, 失败返回-1
3、发送数据
① ssize_t sendto(int sockfd, void *buf, size_t dlen,
int flag, struct sockaddr *peer, socklen_t alen);
4、接收数据
① ssize_t recvfrom(int sockfd, void *buf, size_t dlen,
int flag, struct sockaddr *peer, socklen_t *alen);
5、关闭套接字、释放资源
① int close(int fd)
6、字节序相关接口
五、UDP通信的简单实现
网络通信中的数据都具备完整的五元组(源端IP、对端IP、源端端口、对端端口、协议)
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址,说白了就是为了确定发送用户的IP地址。
有了IP地址之后,是用户主机上的那个进程或者说是应用程序呢?
有了IP地址+端口号 可以唯一确认某一台主机上的某一个进程
例如 192.168.2.2.1234
uint16_t 端口号是一个二字节16位的整数
一个端口号只能被一个进程占用,如果另外一个进程也进行访问这个端口号,就会存在端口冲突的风险
源端端口和对端端口分辨描述了数据是谁发送的,是要发给谁。
网络通信通常是两台主机之间的通信(俩台主机分别为客户端、服务端)
部署用户这边,通常主动发起请求,按下普攻键,给服务端发送一个我需要攻击的目标以及我的攻击力状态,传递一些信息
部署在应用服务提供商一端,被动接受请求,提供服务,收到了你发送的攻击请求,然后对相应的对象进行数据的修改,减掉红Buff野怪35滴血……
网络通信都是客户端与服务端之间的通信,不存在客户端与客户端,服务端与服务端。后者显而易见,前者我攻击一个对象,我不是直接将对方的状态进行修改,而是将这个数据发送给服务端,服务端进行修改之后反映给我攻击的对象的客户端,起到一个中间桥梁的作用。
客户端能够访问的服务端地址信息必须是已知的,才能主动发起请求,并且服务端的信息轻易不会进行修改
因为客户端所访问的服务端比如是存在、已知、且轻易不变的,所以先进行服务端的创建
建立进程与网卡之间的关系,在内核中生成一个套接字结构体(struct scoket)
给创建的套接字结构体,内部存入我需要发送数据的IP地址和端口
在数据传输中通过IP的确认访问到对应的主机网卡,在通过端口信息访问到对应主机的进程或应用程序
例如 192.168.2.3对应的主机进程端口444 发送一个数据给-->192.168.2.2对应主机的端口1234
192.168.2.3.444-->192.168.2.2.1234
struct socket 存放着我们的数据信息还有一个输入缓冲区、一个输出缓冲区
从内核的socket结构体中接受缓冲区中取出数据
将数据放到内核结构体的发送缓冲区
释放资源
(域间通信、ipv4通信、ipv6通信……)其中IPV4(AF_INET)
SOCK_STREAM 流式套接字 提供字节流传输,对应TCP协议
SOCK_DGRAM 数据报套接字 提供数据报传输,对应UDP协议
IPPROTO_TCP 值为6
IPPROTO_UDP 值为17
但是如何通过一个接口完成各种不同地址域类型的地址信息绑定呢?
我们知道ipv4 为uint32_t 具有32位的整形存储 ipv6 uint8_t ip[16] 128位数据 还有其他地址域类型比如域间通信……
原来他们的存储地址空间不同,但是他们都有相同的格式约定,前俩个字节都保存的是地址域类型,当bind进行绑定信息时,会自动识别这俩个字节的地址域类型,然后采取哪种地址结构进行解析了。
那这一过程如何具体实现呢?
使用不同种类型的地址结构时,选择对应的地址类型进行强转传入,例如ipv4的地址结构类型为struct sockaddr_in进行强转。
const char *addr = "192.168.2.2.1234";
(struct sockaddr_in)addr(进行传入即可)
② sockfd socket返回的套接字描述符
③ buf 发送数据的起始地址
④ dlen 发送数据的长度,从buf地址开始,发送dlen长度的数据
⑤ flag 默认为0,阻塞发送(发送缓冲区满了就等着, 当发送缓冲区有空间了进行发送)
⑥ peer 对端地址信息,描述了数据要发送给谁
描述了信息送往哪里 (这里存放的是接收方的信息)
⑦ alen 对端地址信息长度
⑧ 返回值: 成功返回实际发送数据字节长度,失败返回-1;
② sockfd socket创建的套接字描述符
③ 一块地址空间, 里面用来存放我接收得到的数据
④ dlen 接收数据的字节长度
⑤ flag 默认为0,当socket接收缓冲区为空的时候进行等待,有数据了就进行接收
⑥ peer 对端地址信息,描述了接收的数据来自哪里。(这里存放的是来自发送方的地址信息)
⑦ alen 地址信息长度,里面即存放了想要接收的数据长度也存放了实际接收的数据长度
⑧ 成功返回实际接收的数据长度,失败返回-1
①由于网络字节序为大端字节序,所有的网络通信都要遵守这一标准,所以:
如果主机为小端字节序,即进行对应处理,然后进行发送
如果主机为大端字节序,不进行处理
② erhost --> network:
uint32_t htonl(uint32_t hostlong); (四字节)
uint16_t htons(uint16_t hostshort);(二字节)
③ network --> host
uint32_t ntohl(uint32_t netlong);(四字节)
uint16_t ntohs(uint16_t netshort);(二字节)
④ 将点分十进制转换为网络字节序整数
typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *ip);
例如: const char *ip = "192.168.2.2" ==》 0xC0A80202 (192 -> C0 16进制)
⑤ 将网络字节序整数转换为点分十进制
typedef uint32_t in_addr_t;
const char *inet_ntoa(struct in_addr addr);
结构体in_addr内部只有一个变量为 s_addr
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9
10
11
12 int main(int argc, char *argv[])
13 {
14 if(argc != 3)
15 {
16 printf("./udp_srv 192.168.2.2 9000\n");
17 return -1;
18 }
19 uint16_t port = atoi(argv[2]);
20 char *ip = argv[1];
21
22 // 1、创建套接字 int socket(int domain, int type, int protocol)
23 // 参数分别为 地址域格式使用ipv4,使用SOCK_DGRAM数据报套接字类型,使用对应的IPPROTO_UDP 协议
24 int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
25 if(sockfd < 0)
26 {
27 perror("socket error");
28 return -1;
29 }
30
31 // 2、为套接字绑定地址信息
32 // int bind(int s
33 // ockfd, struct sockaddr* addr, socklen_t len);
34 struct sockaddr_in addr;// 定义一个ipv4的地址结构出来
35 addr.sin_family = AF_INET;
36 addr.sin_port = htons(port); // 因为定义的port为俩字节 所以使用htons
37 addr.sin_addr.s_addr = inet_addr(ip);// 将ip点分十进制格式转换为整数形式存储到addr结构> 体中的sin_addr结构体中的变量s_addr
38 socklen_t len = sizeof(struct sockaddr_in);
39 int ret = bind(sockfd, (struct sockaddr*)&addr, len);
40 if(ret == -1)
41 {
42 perror("bind error");
43 return -1;
44 }
45
46 // 3、循环接收发送数据
47 while(1)
48 {
49 // 3.1 接收数据
50 // ssize_t recvfrom(int sockfd, void *buf, int len, int flag, struct sockaddr *peer, socklen_t *len)
51 char buf[1024] = {0};
52 struct sockaddr_in peer;
53 socklen_t len = sizeof(struct sockaddr_in);
54 // peer 中的地址信息是系统进行设置的,数据是谁发的,设置的就是谁的地址
55 ssize_t ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&peer, &len);
56 if(ret < 0)
57 {
58 perror("revcfrom error");
59 return -1;
60 }
61 char *peerip = inet_ntoa(peer.sin_addr);// 将网络字节序转化为字符串
62 uint16_t peerport = ntohs(peer.sin_port);// 将网络字节序端口转化为主机字节序端口
63 printf("client[%s:%d] say:%s\n", peerip, peerport, buf);
64
65 // 4、发送数据
66 char data[1024] = {0};
67 printf("sever say:");
68 fflush(stdout);
69 scanf("%s", data);
70 ret = sendto(sockfd, data, strlen(data), 0, (struct sockaddr*)&peer, len);
71 if(ret < 0)
72 {
73 perror("sendto error");
74 return -1;
75 }
76
77 }
78 close(sockfd);
79 return 0;
80 }