一、理解UDP
1.1 UDP套接字特点
? UDP工作原理就跟寄信一样,写上收信地址以及收信人然后贴上邮票就可以发送了,发送完之后就不管有没有发送到即不确认收信人是否收到信件。与TCP对比,少了可靠性而多了简洁性,UDP不会发送类似ACK这类应答信息,也不会像SEQ那样给数据包分配序列号。在注重效率而非准确性的地方,采用UDP通信是一个很好的选择。
? 为了提供可靠的数据传输服务TCP在不可靠的IP层进行流控制,而UDP就缺少这种流控制机制。流控制是区分UDP与TCP的重要标志,因为TCP生命在于流控制,在上面几章里面讲的“与对方套接字连接以及断开连接过程”也是属于流控制的一部分。
1.2 UDP内部工作原理
UDP最重要的作用就是根据端口号将 传到主机的数据包交给最终UDP套接字,仅此而已。是不是很简单?
二、实现基于UDP的服务器端/客户端
UDP不是面向连接的,因此不需要accept(),listen等函数,但需要绑定UDP套节字。在TCP当中,除了用于监听客户端连接请求的套接字外,其他套接字都是与客户端套接字一一对应的。比如服务端要向10个客户端服务,就需要除守门套接字之外还需10个套接字专门处理与客户端的数据通信。但是在UDP当中,不管是客户端还是服务端都只需要一个套接字就可以。
2.1 操作函数
#include<sys/socket.h>
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
ssize_t recvvfrom(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *from,socklen_t addrlen)
UDP通信核心就是上面两个函数。
接下来我们采用UDP编写回声服务器以及客户端
2.2 基于UDP的回声服务器服务端/客户端
由于没有accept以及listen(),因此此处不存在服务端以及客户端的区别,但是不妨碍我们这样命名,这点需要知道
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.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]));
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<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char *argv[])
{
int client_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr,from_adr;
if(argc!=3)
{
printf("Usage: %s <port>\n",argv[0]);
exit(1);
}
client_sock=socket(PF_INET,SOCK_DGRAM,0);
if(client_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=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(client_sock,message,strlen(message),0,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
str_len=recvfrom(client_sock,message,BUF_SIZE,0,(struct sockaddr*)&from_adr,&clnt_adr_sz);
message[str_len]=0;
printf("Message from server:%s",message);
}
close(client_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
编译
gcc server.c -o server
gcc client.c -o client
-----------------------------------
./server 9091
./client 127.0.0.1 9091
上面是UDP实现的回声服务器、客户端实例,我们需要和之前写的TCP实现的实例作为对比。这里给出TCP服务端与客户端流程:
socket()
|
bind()
|
listen()
|
accept()
|
read()/write()
|
close()
--------------------------------------------------------------------------------------
socket()
|
connect()
|
read()/write()
|
close()
在TCP客户端,我们知道,虽然我们没有手动将套接字与地址绑定(即分配IP与端口号),但是connect()函数自动帮我们完成,也就是说TCP客户端是需要将套接字与IP地址与端口号绑定的,但是在UDP客户端,我们并没有connect()函数,我们也没有手动分配IP地址与端口号,而且测试结果正常,客户端何时分配的IP地址与端口号呢?
2.3 UDP客户端套接字地址的分配
在传输数据(即调用sendto函数)之前应该完成对IP地址的绑定以及端口号;而这个工作我们一般使用bind()函数实现,而且bind()函数在UDP与TCP里面都能使用,并非TCP专有,采用bind()函数分配IP地址与端口号属于手动方式,而在UDP回声服务端\客户端中我们并没有使用bind()函数也能实现数据传输是因为在调用sendto()函数时如果发现尚未分配IP地址与端口号,则在首次调用sendto()函数时会给相应套接字自动分配IP和端口,而且分配地址一直保留到程序结束为止。
三、UDP的数据传输特性和调用connect函数
UDP的数据传输特性是指UDP数据传输存在数据边界(TCP数据传输不存在数据边界),并且我们将在这一节验证这一特性。我们将在在这节讨论UDP中connect函数的调用。
3.1 UDP存在数据边界验证
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char* argv[])
{
int sock;
char message[BUF_SIZE];
struct sockaddr_in my_adr,your_adr;
socklen_t adr_sz;
int str_len,i;
if(argc!=2)
{
printf("Usage:%s <port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_DGRAM,0);
if(sock==-1)error_handling("socket error");
memset(&my_adr,0,sizeof(my_adr));
my_adr.sin_family=AF_INET;
my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
my_adr.sin_port=htons(atoi(argv[1]));
if(bind(sock,(struct sockaddr*)&my_adr,sizeof(my_adr))==-1)error_handling("bind error");
for(int i=0;i<3;i++)
{
sleep(5);
adr_sz=sizeof(your_adr);
str_len=recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&your_adr,&adr_sz);
printf("Message %d:%s\n",i+1,message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char *argv[])
{
int sock;
char msg1[]="Hi!";
char msg2[]="I'm another UDP host!";
char msg3[]="Nice to meet you";
struct sockaddr_in your_adr;
socklen_t your_adr_sz;
if(argc!=3)
{
printf("Usage:%s <port> \n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_DGRAM,0);
if(sock==-1)error_handling("socket() error");
memset(&your_adr,0,sizeof(your_adr));
your_adr.sin_family=AF_INET;
your_adr.sin_addr.s_addr=inet_addr(argv[1]);
your_adr.sin_port=htons(atoi(argv[2]));
sendto(sock,msg1,sizeof(msg1),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
sendto(sock,msg2,sizeof(msg2),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
sendto(sock,msg3,sizeof(msg3),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
host2.c调用三次sendto()函数传输数据,而host1.c调用三次接收数据。需要注意的是在host1.c我们设定了延时sleep(5),也就是每次接收数据间隔为5s,但是在host2.c当中我们并没有设置延时,也就是说在host2.c通过调用三次sendto()函数将数据发送给了host2,如果是TCP,这时只需要调用一次读取函数即可全部读取,UDP则不同,在这种情况下也需要调用3次recvfrom函数依次读取,运行结果如下 所示。
运行结果
//先执行host1.c
./host1 9091
//在执行host2.c
./host2 127.0.0.1 9091
---------------------------------------------
host1收到结果:
Message 1:Hi!
Message 2:I'm another UDP host!
Message 3:Nice to meet you
----------------------------------------------
说明确实调用了三次recvfrom函数用于接收数据
UDP数据报概念
UDP套接字传输的数据包又称为数据报,实际上数据报也是属于数据包的一种。与TCP不同的是其本身可以成为1个完整数据(由于缺少流控制机制)。UDP存在数据边界,一个数据包即可成为一个完整数据因此成为数据报。
3.2 已连接UDP套接字与未连接UDP套接字
TCP套接字中需要注册(绑定)待传数据的目标IP与端口号,而UDP不需要。因此通过sendto()函数传递数据的过程分为以下三步:
- 向UDP套接字自动分配目标IP和端口号(非与套接字绑定)
- 传输数据
- 删除UDP套接字中注册的目标地址信息。
每次调用sendto()函数,上述三个过程都会执行一遍,也就是说调用N次sendto()函数所使用的目标地址信息都不同,因此我们可以用同一套UDP套接字服务多个目标。这种未注册目标地址信息的套接字称为未连接套接字,反之注册了目标地址信息的套接字称为已连接套接字。默认情况下UDP属于未连接套接字,但假如我们使用UDP实现如下场景:
IP为211.210.147.82的主机87号端口号准备了三个数据,调用3次sendto()进行传输。
这种要在三次sendto()使用同一个目标地址信息就很难实现,因此我们需要连接的UDP套接字。
3.3 创建已连接UDP套接字
过程很简单,在TCP套接字当中,我们使用connect注册目标地址信息,而connect函数在UDP与TCP当中都能用,因此我们只需加一个connect()函数就实现了。
sock=socket(PF_INET,SOCK_DGRAM,0);
memset(&adr,0,sizeof(adr));
adr.sin_family=AF_INET;
adr.sin_addr.s_addr=inet_addr("211.210.147.82");
adr.sin_port=htons(atoi("87");
connect(sock,(struct sockaddr*)&adr,sizeof(adr));
注意,UDP使用connect函数注册目标地址信息并不意味着要与对方UDP套接字进行连接。
与未连接套接字不同的事,之后调用sendto()函数进行发送数据时不需要自动分配IP地址以及端口号,因为已经已指定了目标,此时sendto()变成纯粹的传输数据的函数,我们不仅仅能使用sendto()、recvfrom()进行收发数据,我们还能直接使用write()、read()进行通信。下面将2.2 节当中的client.c改写成write()、read()函数进行通信的方式。server.c不变。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char *argv[])
{
int client_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr,from_adr;
if(argc!=3)
{
printf("Usage: %s <port>\n",argv[0]);
exit(1);
}
client_sock=socket(PF_INET,SOCK_DGRAM,0);
if(client_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=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
connect(client_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
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(client_sock,message,strlen(message));
str_len=read(client_sock,message,sizeof(message)-1);
message[str_len]=0;
printf("Message from server:%s",message);
}
close(client_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
|