UDP套接字的特点
UDP通信与传统的信件通信类似。寄件人填写好地址(IP)和收信人姓名(Port)后将信件投递到邮筒(交付给网络层)。不保证信件的的准确到达。提供的是不可靠的传输服务。考虑可靠性TCP肯定好于UDP。但是UDP相对于TCP具有以下优点。
不用连接,无连接时延,实时性高
不用维护连接状态,如确认号,序列号,流量控制,拥塞控制
头部开销小
流控制是区分UDP和TCP的最重要的标志 。 但若从TCP中除去流控制,所剩内容也屈指可数 。也就是说,TCP的生命在于流控制 。
IP的作用就是让离开主机B的UDP数据包准确传递到主机A。但把UDP包最终交给主机A的某一UDP套接字的过程则是由UDP完成的 。UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字 。、
TCP比UDP慢的原因通常有以下两点 。
- 收发数据前后进行的连接设置及清除过程 。
- 收发数据过程中为保证可靠性而添加的流控制
UDP 中的服务器端和客户端没有连接
UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。 也就是说,不必调用TCP连接过程中调用的 listen函数和accept函数 。 UDP中只有创建套接字的过程和数据交换过程 。
UDP 服务器端和客户端均只需 1 个套接字
因此UDP可以实现1对1,1对多,多对1,多对多传输。多个UDP投递到某一主机的数据都可以通过该主机的UDP套接字获取。
基于 UDP 的数据 I/O 函数
创建好TCP套接字后,传输数据时无需再添加地址信息 。 因为l’CP套接字将保持与对方套接字的连接。 换言之 , TCP套接字知道目标地址信息 。 但UDP套接字不会保持连接状态 ( UDP套接字只有简单的邮筒功能),因此每次传输数据都要添加 目标地址信息 。 这相当于寄信前在信件中填写地址 。 接下来介绍填写地址并传输数据时调用的UDP相关函数 。
ssize_t sendto(int sock, void *buff,size_t nbyte,int flags,struct sockaddr *to, socklen_t addrlen);
//成功时返回传输的字节损,失败时返回-1 0
//sock: 用于传输数据的 UDP套接字文件描述符。
//buf: 保存待传输数据的缓冲地址值。
//nbγtes: 待传输的数据长度,以字节为单位。
//flags :可选项参数,若没有则传递0。
//to: 存有目标地址信息的sockaddr结构体变量的地址值。
//addrlen: 传递给参数to的地址值结构体变量长度。
上述函数与之前的TCP输出雨数最大的区别在于 . 此函数需要向它传递目标地址信息 。 接下来介绍接收UDP数据的函数 。 UDP数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回UDP数据包中的发送端信息。
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr * from, socklen_t *addrlen);
//成功时返回接收的字节数,失败时返回-1
//sock :用于接收数据的UDP套接字文件描述符。
//buf :保存接收数据的缓冲地址值。
//nbytes: 可接收的最大字节数,故无法超过参数buf所指的缓冲大小 。
//flags: 可选项参数,若没有则传入0 。
//from: 存有发送端地址信息的 sockaddr结构体变量的地址值。
//addrlen: 保存参数from的结构体变量长度的变量地址值。
编写UDP程序时最核心的部分就在于上述两个函数 , 这也说明二者在UDP数据传输巾 的地位 。
UDP不同于TCP ,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock==-1)
error_handling("UDP socket creation error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);//随机分配服务器网络地址
serv_adr.sin_port=htons(atoi(argv[1]));
//确定服务器UDP套接字的端口号和网络地址
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
while(1)
{
clnt_adr_sz=sizeof(clnt_adr);
//接受客户端数据
str_len=recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
//发送到客户端
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_DGRAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
while(1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&serv_adr, sizeof(serv_adr));
adr_sz=sizeof(from_adr);
str_len=recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
TCP客户端套接字在调用 connect函数时自动分配IP地址和端口号,既然如此, UDP客户端何时分配IP地址和端口号?
调用sendto函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的地址分配过程 。而且此时分配的地址一直保留到程序结束为止,因此也可用来与其他UDP套接字进行数据交换 。 所以之前示例中省略了该过程 ,这也是普遍的实现方式 。
UDP 的数据传输特性和调用 connect 函数
前面说过TCP数据传输中不存在边界,这表示"数据传输过程中调用 I10函数的次数不具有任何意 。"相反UDP是具有数据边界的协议,传输中调用I/O函数的次数非常重要 。 因此,输入函数的调用次数应和输出函数的调用次数完全一致,这样才能保证接收全部已发送数据 。
已连接 (connected)UDP套接字与未连接 (unconnected)UDP套接字
通过sendto函数传输数据的过程大致可分为以下3个阶段 。
第1阶段:向UDP套接字注册目标E和端口号 。
第2阶段:传输数据 。
第3阶段:删除UDP套接字中注册的目标地址信息 。
每次调用 sendto函数时重复上述过程。 每次都变更目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字。反之,注册了目标地址的套接字称为连接connected套接字。 显然, UDP套接字默认属于未连接套接字。
创建己连接 UDP 套接字
创建已连接UDP套接字的过程格外简单,只需针对UDP套接字调用 connect函数 。
sock = socket(PF_INET, SOCK_DGRAM,,0);
memset(&adr, 0,sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = ...
adr.sin_ port =...
connect(sock, (struct sockaddr *) &adr) sizeof(adr));
当然,针对UDP套接字调用connect函数并不意味着要与对方UDP套接字连接 , 这只是向UDP套接字注册目 标E和端 口信息 。之后就与TCP套接字一样,每次调用 sendto函数时只需传输数据 。 因为已经指定了收发对象,所以不仅可以使用sendto 、 recvfrom函数 , 还可以使用write 、 read函数进行通信。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_DGRAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
//创建连接UDP,使用read和write交换数据。此时已经自动分配了客户端的IP和端口号。
//可以用此udp(sock)和其余主机UDP通信
while(1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, sizeof(message)-1);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}