4.服务端完整代码
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define PORT 9999 #define IP "192.168.1.8" #define SIZE 1024 int main(){ int sockfd = -1; int len = sizeof(struct sockaddr); struct sockaddr_in serveraddr = {0}; char buf[SIZE]; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -1; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(IP); if (bind(sockfd, (struct sockaddr*)&serveraddr), sizeof(struct sockaddr)){ perror("fail not bind"); close(sockfd); // 这里要关闭一下套接字,因为已经失败了 return -2; } // 循环读取数据 while(1){ recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, len); printf("[%s %d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), &buf); } return 0; }
5.sendto发送函数
学习了接收函数后我们就可以来学习一下发送函数了,函数也是比较简单,函数原型:
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 功能:发送数据 参数: sockfd:文件描述符,socket的返回值 buf:要发送的数据 len:buf的长度 flags:标志位 0 阻塞 MSG_DONTWAIT 非阻塞 dest_addr:目的网络信息结构体(需要自己指定要给谁发送) addrlen:dest_addr的长度 返回值: 成功:发送的字节数 失败:‐1
使用的代码如下:
printf("请输入你要发送的信息:"); scanf("%s", buf); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len);
这样我们客户端的代码也写完了。
6.客户端完整代码
#include <stdio.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define PORT 9999 #define IP "192.168.1.8" #define SIZE 1024 int main(){ int sockfd = -1; int len = sizeof(struct sockaddr); struct sockaddr_in serveraddr = {0}; char buf[SIZE]; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -1; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(IP); // 循环读取数据 while(1){ printf("请输入你要发送的信息:"); scanf("%s", buf); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len); printf("发送成功\n"); } return 0; }
这里有一个很重要的问题,就是在客户端里不用进行绑定,那如果不进行绑定怎么让程序知道发送给哪个服务端呢?
这里我们不是使用了一个sendto,而这个函数携带了目标机器的配置结构体,它会通过这个结构体去找到目标。
三、TFTP文件接收程序
1.TFTP概述
TFTP是简单文件传送协议,最初用于引导无盘系统,被设计用来传输小文件。
这个协议是基于UDP协议实现的,不用进行用户有效性认证。
传输模式分为三种:
- octet : 二进制模式
- netascii : 文本模式
- mail : 已经不再支持
2.TFTP通讯过程
实现客户端发送一个请求连接的数据包给服务端,服务端收到后验证一下请求包中的内容:
然后服务端收到请求后会发送请求的内容组成的数据包:
当客户端收到数据包后需要向服务端发送ACK响应包:
如果在这个过程中出现了问题,服务端会发送一个错误的数据包:
这里面的错误码有下面的几种:
错误码:
0 未定义,参见错误信息
1 File not found
2 Access violation
3 Disk full or allocation exceeded
4.illegal TFTP operation
5.Unknown transfer ID
6.File already exists
7.No such user
8.Unsuppored option(s) requested
3.TFTP客户端
现在我们来编写一个简单的TFTP客户端来接收服务端的文件,这里的服务端用软件来进行开启,当然你也可以主机写一个,都是使用UDP的,很简单。
流程如下:
代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <fcntl.h> #define SIZE 1024 #define DATASIZE 516 void getFile(int sockfd, struct sockaddr_in serveraddr, char* filename){ char buf[SIZE]; int fileflag = 0; // 用来判断文件是否被创建 int structlen = sizeof(struct sockaddr); int len = 0; // 数据包长度 int fd = -1; int num = 1; // 包序号 int n = 0; // 接收长度 len = sprintf(buf, "%c%c%s%c%s%c", 0, 1, filename, 0, "octet", 0); // 拼接请求数据包 // 发送数据包 sendto(sockfd, buf, len, 0, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)); // 接收数据包 n = recvfrom(sockfd, buf, DATASIZE, 0, (struct sockaddr*)&serveraddr, &structlen); // 判断数据包 if (buf[1] == 5){ // 这个是获取到错误包 printf("error:%s\n", buf+4); // 打印错误信息 return ; } else if (buf[1] == 3){ // 获取数据包 if (fileflag == 0){ // 文件没有被创建 fd = open(filename, O_WRONLY | O_CREAT, 0664); if (fd == -1){ perror("fail not open"); return; } } while(1){ // 将读取的内容写入文件中 if (n == DATASIZE && (ntohs(*(unsigned short*)(buf + 2)) == num)){ // 判断数据长度和包序号 write(fd, buf + 4, n - 4); // 去掉数据头和数据包数 // 发送ACK响应数据包 sendto(sockfd, buf, 4, 0, (struct sockaddr*)&serveraddr, len); num++; } else if (n < DATASIZE && (ntohs(*(unsigned short*)(buf + 2)) == num)){ // 数据包长度小于516,接收结束 write(fd, buf + 4, n - 4); sendto(sockfd, buf, 4, 0, (struct sockaddr*)&serveraddr, len); break; } } close(fd); // 关闭文件 printf("传输完成\n"); } return ; } int main(int argv, char* argc[]){ int sockfd = -1; struct sockaddr_in serveraddr = {0}; // TFTP服务端配置结构体 if (argv != 3){ printf("./TFTP <IP> <Filename>"); return -1; } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -2; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(69); // TFTP默认开启69号端口 serveraddr.sin_addr.s_addr = inet_addr(argc[1]); getFile(sockfd, serveraddr, argc[2]); // 获取文件的函数 close(sockfd); return 0; }
这里使用服务端的软件是:TFTPd32
,下载的链接:tftpd32
上面写的只是下载的代码,当然也可以写一个上传的功能,其实就是将write改变为read,然后通过数据包发送过去即可。
四、UDP广播
1.什么是广播
广播是由一台主机向该主机所在子网内的所有主机发送数据的方式,例如192.168.1.1主机发送广播消息,192.168.1.1~192.168.1.254都能收到数据。
广播只能用UDP或原始IP实现,不能用TCP。
2.广播特点和用途
用途主要是单个服务器与多个客户主机通信时减少分组流通,下面的几个协议都用到广播:
1、地址解析协议(ARP)
2、动态主机配置协议(DHCP)
3、网络时间协议(NTP)
广播局限于在局域网内使用,离开了局域网后就没有办法进行使用了。
3.广播地址
广播地址分为两种,一种是定向广播地址,另一种是受限广播地址。
定向广播地址是主机ID全为1,比如一个局域网内,IP地址是192.168.1.0,子网掩码是:255.255.255.0,那这个网段中的定向广播地址为:192.168.1.255。
而受限广播地址是:255.255.255.255,路由器从来不转发该广播。
4.广播、单播和多播
4.1 单播
单播是什么?我们可以拿一张图来解释一下:
这个过程和之前的UDP通讯一样,这个单播的MAC地址必须要是对方的MAC地址,如果MAC地址不是对方的MAC地址那对方就不会再继续解析。
4.2 广播
在广播中,MAC地址是全为1的,对方接收到这个数据包中如果看到MAC地址全为1,不是和自己一样的也会继续解析,相当于就忽略了检查MAC地址的这一步,然后继续解析。
编写一个广播的代码是很简单的,实现我们要了解一下编写的流程:
发送者:
1、创建套接字
2、设置为允许发送广播权限
3、发送内容
接收者:
1、创建套接字
2、将套接字与广播的消息结构体绑定到一起
3.接收消息
4.2.1 设置为允许发送广播权限
这个需要使用到一个函数setsockopt
,这个函数在设置广播和多播的时候会使用到,所以我们先了解一下这个函数,函数的原型如下:
#include <sys/socket.h> int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); 功能:设置一个套接字的选项(属性) 参数: socket:文件描述符 level:协议层次 SOL_SOCKET 套接字层次 IPPROTO_TCP tcp层次 IPPROTO_IP IP层次 option_name:选项的名称 SO_BROADCAST 允许发送广播数据(SOL_SOCKET层次的) option_value:设置的选项的值 int类型的值,存储的是bool的数据(1和0) 0 不允许 1 允许 option_len:option_value的长度 返回值: 成功:0 失败:‐1
比如说我们要设置一个套接字为发送广播权限,代码可以这样写:
int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt)); • 1 • 2
这样就可以设置好发送广播了。
4.2.2 发送端代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #define SIZE 1024 int main(int argv, char* argc[]){ int sockfd = -1; int on = 1; int len; int ret = -1; char buf[SIZE]; struct sockaddr_in serveraddr = {0}; if (argv != 3){ printf("./Send <IP> <PORT>"); return -1; } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("sockfd not ok"); return -2; } // 设置发送广播权限 ret = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)); if (ret == -1){ perror("fail not setsockopt"); close(sockfd); return -3; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argc[2])); serveraddr.sin_addr.s_addr = inet_addr(argc[1]); len = sizeof(struct sockaddr); while(1){ // 发送数据 printf("you talk:"); scanf("%s", buf); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, len); } close(sockfd); return 0; }
4.2.3 接收端完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #define SIZE 1024 int main(int argv, char* argc[]){ int sockfd = -1; int len; int n; char buf[SIZE]; struct sockaddr_in serveraddr = {0}; // 广播的结构体 if (argv != 3){ printf("./recv <IP> <PORT>\n"); return -1; } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -2; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argc[2])); serveraddr.sin_addr.s_addr = inet_addr(argc[1]); if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){ perror("fail not bind"); close(sockfd); return -3; } len = sizeof(struct sockaddr); while(1){ // 接收信息 n = recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len); if (n <= 0){ printf("no"); break; } printf("he talk:%s\n", buf); } close(sockfd); return 0; }
4.3 多播
多播其实就算一个发送端给多个接收端发送消息,但是这些接收端必须是在一个组内才能接收这个消息。
而多播发送端的地址需要从224.0.0开始到239.255.255.254结束,也就是你在设置发送端的时候需要将发送端的IP设置为这个范围内才可以。
多播工作流程如下:
4.3.1 多播编写流程
发送者:
1、创建套接字
2、发送数据
接收者:
1、创建套接字
2、设置为加入多播组
3、将套接字和多播消息绑定到一起
4、接收数据
这里看起来很麻烦,特别是设置为加入多播组该如何设置,其实还是用到上面使用的setsockopt
函数来进行设置,那如何设置呢?
#include <sys/socket.h> int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len); 功能:设置一个套接字的选项(属性) 参数: socket:文件描述符 level:协议层次 IPPROTO_IP IP层次 option_name:选项的名称 IP_ADD_MEMBERSHIP 加入多播组 option_value:设置的选项的值 struct ip_mreq { struct in_addr imr_multiaddr; //组播ip地址 struct in_addr imr_interface; //主机地址 INADDR_ANY 任意主机地址(自动获取你的主机地址) }; option_len:option_value的长度 返回值: 成功:0 失败:‐1
比如说我将创建好的套接字加入到多播组中,代码就如下:
struct ip_mreq mreq = {0}; mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1"); // 组播的IP地址 mreq.imr_interface.s_addr = INADDR_ANY; setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq));
这样就将接收端加入进多播组中了。
4.3.1 发送端完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #define SIZE 1024 int main(int argv, char* argc[]){ int sockfd = -1; char buf[SIZE]; struct sockaddr_in serveraddr = {0}; if (argv != 3){ printf("./group_send <IP> <PORT>\n"); return -1; } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -2; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argc[2])); serveraddr.sin_addr.s_addr = inet_addr(argc[1]); while(1){ printf("you talk:"); scanf("%s", buf); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); } return 0; }
4.3.2 接收端完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #define SIZE 1024 int main(int argv, char* argc[]){ int sockfd = -1; int len; char buf[SIZE]; struct ip_mreq mreq = {0}; struct sockaddr_in serveraddr = {0}; if (argv != 3){ printf("./group_recv <IP> <PORT>\n"); return -1; } sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1){ perror("fail not socket"); return -2; } // 添加一个多播组IP mreq.imr_multiaddr.s_addr = inet_addr(argc[1]); // 添加一个将要添加到多播组的IP mreq.imr_interface.s_addr = INADDR_ANY; if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))){ perror("fail not setsockopt"); close(sockfd); return -3; } serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argc[2])); serveraddr.sin_addr.s_addr = inet_addr(argc[1]); if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){ perror("fail not bind"); close(sockfd); return -4; } len = sizeof(struct sockaddr); while(1){ recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len); printf("[IP:%s PORT:%d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), buf); } return 0; }
总结
UDP通信就介绍完了,其实UDP通信是很简单的,相比于TCP,UDP的客户端不需要进行连接,只需要使用sendto函数就可以将数据发送给客户端了,大家只需要多多练习就好了,再过几天我给大家介绍一下TCP的通讯,这样就能对UDP有更深的理解了。