目录
前言
一、基础知识
1、跨主机传输
1、字节序
2、主机字节序和网络字节序
3、IP转换
2、套接字
3、什么是UDP通信
二、如何实现UDP通信
1、socket():创建套接字
2、bind():绑定套接字
3、sendto():发送指定套接字文件数据
4、recvfrom():接收指定地址信息的数据
三、具体实现代码
前言
在前面我们知道,在使用UDP通信是在传输层选择使用UDP协议,并且在传输层只有两个协议,分别是UDP和TCP协议,在本节中,我们就来学习如何实现UDP通信
一、基础知识
1、跨主机传输
1、字节序
字节序:不同类型的CPU主机,内存存储多字节数据时的存在不同序列存储方式
a、小端字节序(小端存储):低序字节存储在内存低地址上,高序字节存储在内存高地址上
b、大端字节序(大端存储):低序字节存储在内存高地址上,高序字节存储在内存低地址上
short、int、long 有字节序的概念
char、float、字符串没有字节序的说法
如何查看电脑是大端存储还是小端存储
//查看电脑大端存储还是小端存储 #include<stdio.h> int main(int argc, const char *argv[]) { int a=0x87654321; char *p=&a; printf("a=%#x\n",*p); return 0; }
我的电脑输出的是a=0x21,说明是小段存储,其实大多数电脑都是小端存储
2、主机字节序和网络字节序
- 主机字节序:主机本身在计算机中存储多字节数据的方式(大端、小端:CPU)现目前电脑一般都是小端
- 网络字节序:数据在网络中规定的传输方式,网络字节序使用大端字节序方式传输
所以在跨主机传输过程中,需要使用统一的字节序,即网络字节序,避免兼容性问题
主机字节序转换为网络字节序,便于传输:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); 功能:把 hostlong 主机字节序整型转换为网络字节序,返回值就是网络字节序整数 uint16_t htons(uint16_t hostshort); 功能:把 hostshort 主机字节序短整型转换为网络字节序,返回值就是网络字节序短整数
网络字节序转换为主机字节序,便于解析识别:
#include <arpa/inet.h> uint32_t ntohl(uint32_t netlong); 功能:把 netshort 网络字节序 整型 转换为主机存储的主机字节序,返回值就是 主机字节序整数 uint16_t ntohs(uint16_t netshort); 功能:把 netshort 网络字节序短整型 转换为主机存储的主机字节序,返回值就是 主机字节序
3、IP转换
在主句传输数据时会对大于两个字节的数据进行网络字节序的转换,那么IP地址通常是大于两个字节的,也同样要进行IP转换
IP地址整数转换为二进制网络字节序的IP地址 :
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char *cp); 功能:将 ip地址字符串 转换为 IP地址整数(网络字节序IP地址) 参数: const char *cp:要转换的 IP地址的点分十进制字符串首地址 返回值: 成功,返回 转换后的 网络字节序的IP地址 typedef uint32_t in_addr_t;
IP地址的二进制网络字节序转换为IP地址的整数:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> char *inet_ntoa(struct in_addr in); 功能:把 IP地址转换为 点分十进制字符串格式 参数: struct in_addr in:指定要转换的IP地址 的结构体类型,结构体中的成员为 IP地址 类型: typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr;//IP地址 网络字节序 整数 };
2、套接字
最早套接字和共享内存、消息队列、管道一样,只能实现一个主机内部的进程间通信,随着TCP/IP网络模型的引入,使得套接字能够支持不同主机之间的进程间通信,socket函数,创建一个套接字文件,可以在内核空间中创建两块缓冲区,用于发送数据,接收数据。也包含对应的TCP/IP协议规则
使用socket()函数创建套接字文件
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); 功能:创建socket套接字,用于网络通信 参数: 参数1: int domain:地址族,协议族 AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) 参数2: int type:类型 SOCK_STREAM:字节流套接字,流式套接字,默认使用TCP协议 SOCK_DGRAM:数据报套接字,报式套接字,默认使用UDP协议 SOCK_RAW:原始套接字,其协议需要在第三个参数中指定 参数3: int protocol:协议 0:使用默认协议 IPPROTO_TCP IPPROTO_UDP 返回值: 成功,返回 套接字文件描述符(套接字) 失败,返回-1,设置errno
3、什么是UDP通信
根据传输层的协议不同,通信的实现、通信的方式也各不相同,是不同的方式完成通信
传输层:TCP、UDP
通信方式有两种:UDP通信 与 TCP通信
UDP通信的步骤:
二、如何实现UDP通信
1、socket():创建套接字
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); 功能:创建socket套接字,用于网络通信,在内核空间中创建套接字文件(有两个缓冲区:发送缓冲区、接收缓冲区),返回该套接字文件缓冲区的文件描述符 参数: 参数1: int domain:地址族,协议族 AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) 参数2: int type:类型 SOCK_STREAM:字节流套接字,流式套接字,默认使用TCP协议 SOCK_DGRAM:数据报套接字,报式套接字,默认使用UDP协议 SOCK_RAW:原始套接字,其协议需要在第三个参数中指定 参数3: int protocol:协议 0:使用默认协议 IPPROTO_TCP IPPROTO_UDP 返回值: 成功,返回 套接字文件描述符(套接字) 失败,返回-1,设置errno
2、bind():绑定套接字
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:绑定地址信息到指定的套接字文件描述符上,为套接字通信指定使用的IP、port 参数: 参数1: int sockfd:指定要将地址信息绑定到哪个套接字上,套接字文件描述符 参数2: const struct sockaddr *addr:地址信息结构体的指针,通用的一个地址结构体,由于存在不同的地址族选择,所以地址的表示方式不一样,为了统一表示,所以参数为通用的地址信息结构体类型 用来表示有一个地址信息结构体:通用结构体 struct sockaddr { sa_family_t sa_family; char sa_data[14]; } 真实的地址信息结构体需要根据地址族来指定 不同的地址族有不同的地址信息结构体 AF_INET地址族的地址信息结构体: struct sockaddr_in { sa_family_t sin_family;//指定地址族,AF_INET in_port_t sin_port;//端口号的网络字节序,2个字节 struct in_addr sin_addr;//使用的ip地址的网络字节序(结构体类型) }; struct in_addr {//ip地址的网络字节序结构体 uint32_t s_addr;//ip地址网络字节序 }; 参数3: socklen_t addrlen:真实的地址信息结构体大小 返回值: 成功,返回0 失败,返回-1,设置errno
3、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); 功能:发送数据给指定的接收方,即当前进程套接字发送数据给指定的ip、port进程 参数: 参数1: int sockfd:套接字,通信:使用指定的套接字描述符来发送数据(数据传入哪个套接字的缓冲区用于发送) 参数2: const void *buf:指定要发送的数据的首地址(把指定地址的数据进行发送) 参数3: size_t len:发送多少个字节 参数4: int flags:选项 0:阻塞方式发送,当缓冲区满,阻塞等待,不继续执行 MSG_DONTWAIT:非阻塞方式发送,当缓冲区满,不等待,返回错误失败 参数5: const struct sockaddr *dest_addr:地址信息结构体,指定将数据发送给谁(ip、port),填写对方的地址信息 地址信息根据地址族不同,结构体信息内容不同 如果: AF_INET AF_INET地址族的地址信息结构体: struct sockaddr_in { sa_family_t sin_family;//指定地址族,AF_INET in_port_t sin_port;//端口号的网络字节序,2个字节 struct in_addr sin_addr;//使用的ip地址的网络字节序(结构体类型) }; struct in_addr {//ip地址的网络字节序结构体 uint32_t s_addr;//ip地址网络字节序 }; 参数6: socklen_t addrlen:真实的地址信息结构体的大小 返回值: 成功,返回发送的字节数 失败,返回-1,设置errno
4、recvfrom():接收指定地址信息的数据
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能:接收数据包,同时可以接收到数据包从哪里来(额可以获取发送方的地址信息) 参数: 参数1: int sockfd:通信使用的套接字文件描述符,指定获取哪个套接字的数据(对方发送发送到ip、port的套接字缓冲区) 参数2: void *buf:存储读取到的数据,接收的数据存储的地址 参数3: size_t len:要读取多少个字节 参数4: int flags:选项 0:阻塞方式接收,当缓冲区为空,没有接收到数据时,阻塞等待,不继续执行 MSG_DONTWAIT:非阻塞方式接收,当缓冲区为空(没有数据),不等待,返回错误失败 参数5: struct sockaddr *src_addr:地址信息结构体,不同的地址族地址信息结构体不同,不同的地址族使用对应的结构体来存储,发送方的地址信息 如果不想知道发送方的地址信息,则填NULL 参数6: socklen_t *addrlen:地址信息结构体的大小,指针对应空间存储 如果不想获取,则填NULL 返回值: 成功,返回收到的字节数 失败,返回-1,设置错误码
三、具体实现代码
在实现通信之前,我们使用下面网络调试助手来进行通信传输
通过网盘分享的文件:scomm.exe
链接: https://pan.baidu.com/s/1OkiZLT_CeoryEZepaOSGqQ 提取码: 8a85
首先,更改下面的代码
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <pthread.h> #define PORT 20000 #define DEST_PORT 10000 #define DEST_IP "192.168.124.29" #define BUF_SIZE 128 int sockfd; struct sockaddr_in destaddr; void *send_thread(void *arg) { char buf[BUF_SIZE]; while (1) { bzero(buf, BUF_SIZE); fgets(buf, BUF_SIZE, stdin); sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&destaddr, sizeof(struct sockaddr_in)); } return NULL; } void *recv_thread(void *arg) { char buf[BUF_SIZE]; while (1) { struct sockaddr_in rcv_addr; socklen_t rcv_addr_len = sizeof(rcv_addr); int size = recvfrom(sockfd, buf, BUF_SIZE - 1, 0, (struct sockaddr*)&rcv_addr, &rcv_addr_len); if (size > 0) { buf[size] = '\0'; printf("Received: %s\n", buf); } } return NULL; } int main(int argc, const char *argv[]) { // 创建套接字,使用UDP通信 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket creation failed"); return -1; } // 绑定套接字 struct sockaddr_in udpaddr; udpaddr.sin_family = AF_INET; udpaddr.sin_port = htons(PORT); udpaddr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr*)&udpaddr, sizeof(struct sockaddr_in)) < 0) { perror("bind failed"); close(sockfd); return -1; } // 设置对方的信息 destaddr.sin_family = AF_INET; destaddr.sin_port = htons(DEST_PORT); destaddr.sin_addr.s_addr = inet_addr(DEST_IP); // 创建发送和接收线程 pthread_t send_tid, recv_tid; pthread_create(&send_tid, NULL, send_thread, NULL); pthread_create(&recv_tid, NULL, recv_thread, NULL); // 等待线程完成 pthread_join(send_tid, NULL); pthread_join(recv_tid, NULL); // 关闭套接字 close(sockfd); return 0; }
但是代码中的IP地址要更改
更改步骤:
1、将下面的DEST_IP改为打开网络调试助手的IP地址,
如何查找打开的网络调试助手的代码和端口
在这里查看网络端口和接收端的IP地址,每个人的不一样,因此要读者自己去设置