UDP编程之echo服务器
前言
UDP为无连接不可靠的数据报协议,不同于TCP提供的面向连接的可靠字节流。
客户端不需要与服务器事先建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定服务器的地址作为参数。
服务器不接受来自客户的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将与所接收的数据报一道返回客户的协议地址,因此服务器可以把相应发送给正确的客户。
系统调用
发送
#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:描述符
- buf:指向待发送的缓冲区的指针
- len:待发送缓冲区数据的长度
- flags:发送数据的标志位,目前设置为0
- dest_addr:服务器的套接字地址结构(IP地址和端口号)
- addrlen:上一个参数dest_addr的大小
返回值
发送出去的数据的大小。
接收
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数
- sockfd:描述符
- buf:指向待接收数据的缓冲区指针
- len:最大接收的数据的长度
- flags:标志位,目前设置为0
- src_addr:传出参数,指定了客户端的套接字地址结构;如果该参数为空指针,代表调用者对客户端的地址不感兴趣,那么对应的addrlen参数也应该为空指针
- addrlen:传出参数,上一个参数src_addr的大小
返回值
接收的数据的大小。
代码示例
服务端
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <limits.h>
#define MAXLINE 4096
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in servaddr, clientaddr;
int listen_port;
if(argc != 2)
{
printf("Usage:./a.out <ListenPort>\n");
exit(EXIT_FAILURE);
}
listen_port = atoi(argv[1]);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket error");
exit(EXIT_FAILURE);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(listen_port);
if(bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
perror("bind error");
exit(EXIT_FAILURE);
}
// 回射服务器处理流程
int n, len;
char msg[MAXLINE]; // 接收客户端的消息
for(;;)
{
len = sizeof(clientaddr);
n = recvfrom(sockfd, msg, MAXLINE, 0, (struct sockaddr*)&clientaddr, &len);
sendto(sockfd, msg, n, 0, (struct sockaddr*)&clientaddr, len);
}
}
客户端
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <limits.h>
#define MAXLINE 4096
int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in servaddr, clientaddr;
int server_port;
if(argc != 3)
{
printf("Usage:./a.out <ServerAddr> <ServerPort>\n");
exit(EXIT_FAILURE);
}
server_port = atoi(argv[2]);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket error");
exit(EXIT_FAILURE);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
servaddr.sin_port = htons(server_port);
// connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
// 回射服务器处理流程
int n, len;
char sendline[MAXLINE], recvline[MAXLINE];
char buf[MAXLINE];
while(fgets(sendline, sizeof(sendline), stdin))
{
sendto(sockfd, sendline, strlen(sendline), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
n = recvfrom(sockfd, recvline, sizeof(recvline), 0, (struct sockaddr*)&clientaddr, &len);
recvline[n] = 0;
fputs(recvline, stdout);
}
}
注意点
- 使用UDP写一个长度为0的数据报是可行的,这会形成一个只包含IP首部(对于IPv4通常为20字节,对于IPv6通常为40字节)和一个8字节的UDP首部而没有数据的IP数据包。这也意味着对于UDP,recvfrom返回0值是可接收的:它不像TCP套接字上read返回0值代表对端已关闭,既然UDP是无连接的,因此也就没有诸如关闭一个UDP连接之类的事情。
- 一般来说,大多数TCP服务器是并发的,大多数UDP服务器是迭代的。
- 由已连接UDP套接字(调用connect函数)引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
- 已连接UDP套接字(调用connect函数),和TCP不同,没有三路握手过程,内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的IP地址和端口号(取自传递给connect的套接字地址结构),然后立即返回到调用进程。
- 对于已连接的UDP套接字,不能使用sendto,而改用write或send。
- 对于已连接的UDP套接字,不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。
- 当应用进程知道要给同一目的地址发送多个数据报时,显式连接套接字效率更高。临时未连接的UDP套接字大约会耗费每个UDP传输三分之一的开销。
- UDP缺乏流量控制。