基于 UDP 的服务端和客户端
UDP 就相当于以前的信件邮寄,我们将信件的地址信息填好,放入邮筒以后,信件是否到达指定的地方我们是无法知道的,信件是否完好无损我们也是无法知道的,信件是否丢失也无法知道。UDP 就是只管将消息发送出去,而不管接收者是否接收到的一种套接字。
UDP 不像 TCP 那样需要建立连接,TCP 因为建立连接,套接字就知道目标地址信息,而 UDP 是无法知道地址信息,因此在进行通信时需要知道目标的地址信息。如下函数中需要传入目标地址信息
- Linux 中的接收和发送数据函数接口
#include <sys/socket.h>
// @return 成功返回传输的字节数,失败返回 -1
// @ params:
// sock: 用于传输数据的 UDP 套接字文件描述符
// buff: 保存待传输数据的缓冲地址值
// nbytes: 待传输的数据长度,以字节为单位
// flags: 可选项参数,若没有则传递 0
// to/from: 存有目标地址信息的 sockaddr 结构体变量的地址值
// adrlen: 传递给参数 to 的地址值结构体变量长度
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *to, socklen_t addrlen);
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
- Windows 中的接收和发送数据函数接口
#include <winsock2.h>
// @return 成功返回对应的字节数,失败返回 -1
// @ params:
// sock: 用于传输数据的 UDP 套接字文件描述符
// buff: 保存待传输数据的缓冲地址值
// nbytes: 待传输的数据长度,以字节为单位
// flags: 可选项参数,若没有则传递 0
// to/from: 存有目标地址信息的 sockaddr 结构体变量的地址值
// adrlen: 传递给参数 to 的地址值结构体变量长度
int sendto(SOCKET sock, const char *buff, size_t nbytes, int flags,
const struct sockaddr *to, int addrlen);
int recvfrom(SOCKET sock, const char *buff, size_t nbytes, int flags,
const struct sockaddr *from, int *addrlen);
UDP 的服务端实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (2 != argc) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
// 2. 使用 bind 分配 IP 地址,减轻 sendto 的功能
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(atoi(argv[1]));
if (-1 == bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr))) {
perror("bind() error");
exit(EXIT_FAILURE);
}
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t addr_len = sizeof(caddr);
while (1) {
char message[1024] = {0};
int ret = recvfrom(sockfd, message, 1024, 0, (struct sockaddr *)&caddr, &addr_len);
if (-1 == ret) {
perror("recvfrom() error");
close(sockfd);
exit(EXIT_FAILURE);
}
sendto(sockfd, message, 1024, 0, (struct sockaddr *)&caddr, addr_len);
}
close(sockfd);
return 0;
}
UDP 的客户端实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (3 != argc) {
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
caddr.sin_family = AF_INET;
caddr.sin_addr.s_addr = inet_addr(argv[1]);
caddr.sin_port = htons(atoi(argv[2]));
while (1) {
printf("Input message(q/Q to quit): ");
char message[1024] = {0};
fgets(message, 1024, stdin);
if (!strcmp(message, "Q\n") || !strcmp(message, "q\n"))
break;
sendto(sockfd, message, 1024, 0, (struct sockaddr *)&caddr, sizeof(caddr));
socklen_t addr_len = sizeof(caddr);
recvfrom(sockfd, message, 1024, 0, (struct sockaddr *)&caddr, &addr_len);
printf("message from server: %s\n", message);
}
close(sockfd);
return 0;
}
sendto
在发现尚未分配地址信息,自动给套接字分配 IP 地址和端口,但是 TCP 中通过 bind
和 connect
进行地址信息的分配,在 UDP 中也可以使用这两个函数。如果 UDP 的服务需要长时间通信,则建议使用 connect
或 bind
进行地址分配,简化 sendto
的功能,能提升程序的整体效率。
UDP 的数据传输特性
UDP 的数据传输特性不同于 TCP,UDP 数据是存在数据边界的,简单的说 UDP 中发几次数据,就得分几次接收,通过下面的程序进行测试。
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (2 != argc) {
fprintf(stderr, "Usage: %s <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
// 2. 使用 bind 分配 IP 地址,减轻 sendto 的功能
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(atoi(argv[1]));
if (-1 == bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr))) {
perror("bind() error");
exit(EXIT_FAILURE);
}
char message[1024];
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t addr_len = sizeof(caddr);
for (int i = 0; i < 3; ++i) {
sleep(5);
int ret = recvfrom(sockfd, message, 1024, 0, (struct sockaddr *)&caddr, &addr_len);
printf("Message %d: %s\n", i+1, message);
}
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]) {
if (3 != argc) {
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket() error");
exit(EXIT_FAILURE);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(argv[1]);
saddr.sin_port = htons(atoi(argv[2]));
socklen_t addr_len = sizeof(saddr);
if (-1 == connect(sockfd, (struct sockaddr *)&saddr, addr_len)) {
perror("connect() error");
close(sockfd);
exit(EXIT_FAILURE);
} else {
printf("Connected ......\n");
}
char msg1[] = "Hi!";
char msg2[] = "I'm another UDP host";
char msg3[] = "Nice to meet you";
sendto(sockfd, msg1, sizeof(msg1), 0, (struct sockaddr *)&saddr, sizeof(saddr));
sendto(sockfd, msg2, sizeof(msg1), 0, (struct sockaddr *)&saddr, sizeof(saddr));
sendto(sockfd, msg3, sizeof(msg1), 0, (struct sockaddr *)&saddr, sizeof(saddr));
close(sockfd);
return 0;
}
运行结果,客户端在启动的一瞬间就结束,服务器端会间隔 5 秒显示客户端发来的 3 条数据。