通俗的理解,在不考虑传输速率的情况下,可以把UDP理解为信箱,寄信前信封上写上寄信人和收信人地址,之后贴上邮票就可以,我们无法知道对方是否收到。信件丢失也不知道,传输方式不可靠。
只考虑可靠性,TCP确实比UDP好。但是UDP结构上比TCP简洁,而且不会发送类似ACK的应答消息,也不会给数据包分配序号。所以有时UDP性能比TCP高出很多。编程中实现UDP也比TCP简单。UDP可靠性虽然比不上TCP,但也不会像想象中的频繁发生数据损毁。在重视性能的情况下UDP比TCP更好。
为了提供可靠的数据传输服务,TCP在不可靠的IP层进行流控制,而UDP缺少这种流控制机制。流控制是区分UDP和TCP的重要标志,TCP与对方套接字建立连接和断开连接过程也属于流控制的一部分。
TCP的速度无法超过UDP,但是在收发某些类型的数据时可能接近UDP。
每次交换的数据量越大,TCP传输速度就越接近UDP传输速度。
IP的作用是让离开主机B的UDP数据包准确传输到主机A。但是把UDP包最终交给某主机A的某一UDP套接字的过程是UDP完成的。UDP最重要的作用是根据端口号将传到主机的数据包交付给最终的UDP套接字。
假设要传输压缩文件,如果传输过程丢失一小部分就可能很难解压,这时就需要TCP连接方式,但是像视频和音频这种数据就算丢失一部分数据,顶多就是卡顿,这时速度要求更多一下,如果还进行流控制,显得很多余这时候就用UDP。
UDP并非每次都快于TCP。TCP比UDP慢的原因
收发数据前后进行的连接设置以及清除过程。
收发数据过程中为保证可靠性添加的流控制。
UDP服务器/客户端不像TCP那样在连接状态下交换数据,因此和TCP不同,无需经过连接过程。不需要调用accept和listen函数。UDP只需创建套接字和数据交换。
TCP中,套接字之间是一对一关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但是在UDP中,不管是服务器端还是客户端都只需要1个套接字。只要一个套接字就可以向任意主机传输数据。
创建好TCP之后,传输数据时无需再添加地址信息。因为TCP套接字将保持与对方的连接。TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,因此每次传输数据都要添加目标地址信息。
下面是填写地址并传输数据时调用的UDP函数
#include <sys/socket>
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
成功返回传输的字节数,失败返回-1
sock 用于传输数据的UDP套接字文件描述符
buff 保存待传输数据的缓冲地址值
nbytes 待传输的数据长度,以字节为单位
flags 可选项参数,若没有传递0
to 存有目标地址信息的sockaddr结构体变量的地址值
addrlen 传递给参数to的地址值结构体变量长度
与TCP输出函数最大区别在于,这个函数需要向他传递目标地址信息。
UDP数据发送端并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回UDP数据包中的发送端信息。
#include<sys/socket.h>
ssize_t recvfrom(int sock,void *buff,size_t nbytes ,int flags,struct sockaddr *from,socklen_t *addrlen);
成功返回接收的字节数,失败返回-1
sock 用于接收数据的UDP套接字文件描述符
buff 保存接收数据的缓冲地址值
nbytes 可接受的最大字节数,所以无法超过参数buff所指的缓冲大小
flags 可选参数,若没有传入0
from 存有发送端信息的sockaddr结构体变量的地址值
addrlen 保存存有参数from结构体变量长度的变量地址值
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);//创建UDP套接字参数为SOCK_DGRAM
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 <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);
}
仔细观察UDP客户端,会发现缺少分配IP地址和端口给套接字的过程。TCP客户端是通过connect自动完成这个步骤,那么UDP啥时候分配IP和端口号呢?
UDP程序中,在调用sento函数之前就要完成套接字的地址分配工作,因此调用bind函数。bind不区分TCP和UDP,也就是说UDP中也可以调用。另外代用sendto函数如果尚未分配地址信息,则首次调用sendto函数时,会给相应套接字自动分配IP地址和端口号。此时分配的地址会一直保留到程序运行结束,因此可用来与其他UDP套接字的数据交换。IP用主机IP,端口号用尚未使用的端口号。 总之调用sendto函数会自动分配IP地址和端口号,因此UDP客户端中通常无需额外的地址分配。
UDP是有数据边界的协议,传输中调用I/O函数次数很重要。输入函数的调用次数应该和输出函数的调用次数完全一致,这样才能保证接收全部的已发送的数据。
#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];
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(i=0; i<3; i++)
{
sleep(5); // delay 5 sec.
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);
}
上面代码每次for循环都间隔了5秒
#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 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 <IP> <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);
}
recvfrom函数调用间隔为5秒,因此调用recvfrom函数前已经调用了3次shento函数。数据已经传输到了host1.如果是TCP,这时候调用一次read函数就能接收全部数据。UDP不同,这种情况下也需要调用3次recvfrom函数。
UDP套接字传输的数据包又称为数据报,实际上数据报也属于数据报的一种。只是与TCP包不同,其本身可以成为1个完整的数据。
这与UDP的数据传输特性有关,UDP存在数据边界,1个数据包即可成为一个完整的数据,因此称为数据报。
TCP套接字中需要注册待传输数据的目标IP和端口号,而UDP中则无需注册,通过sendto函数传输数据过程大体为三个阶段:
第一阶段: 向UDP套接字注册目标IP和端口号
第二阶段: 传输数据
第三阶段: 删除UDP套接字中注册的目标地址信息
每次调用sendto函数重复上述过程。每次都更换目标地址,因此可重复利用UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,就是注册了目标地址信息的套接字称为已连接套接字。显然默认情况下UDP套接字属于未连接套接字。
但是如果与同一个主机进行长时间通信,缩短第一阶段和第三阶段的时间将大大提高性能。
只需针对UDP套接字调用connect函数
sock = sock(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 *)&addr,sizeof(adr));
调用connect函数不代表要与对方建立连接,只是为了注册目标IP地址和端口号
#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));
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));
*/
write(sock, message, strlen(message));
/*
adr_sz=sizeof(from_adr);
str_len=recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
*/
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);
}
#include<winsock2.h>
int sendto(SOCKET s,const char* buf,int len,int flags,const struct sockaddr* to,int tolen);
成功返回传输的字节数,失败返回SOCK_ERROR
#include<winsock2.h>
int recvfrom(SOCKET s,char * buf,int len,int flag,struct sockaddr * from,int *fromlen);
成功返回接收的字节数,失败返回SOCK_ERROR
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET servSock;
char message[BUF_SIZE];
int strLen;
int clntAdrSz;
SOCKADDR_IN servAdr, clntAdr;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
servSock=socket(PF_INET, SOCK_DGRAM, 0);
if(servSock==INVALID_SOCKET)
ErrorHandling("UDP socket creation error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));
if(bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
while(1)
{
clntAdrSz=sizeof(clntAdr);
strLen=recvfrom(servSock, message, BUF_SIZE, 0,
(SOCKADDR*)&clntAdr, &clntAdrSz);
sendto(servSock, message, strLen, 0,
(SOCKADDR*)&clntAdr, sizeof(clntAdr));
}
closesocket(servSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET sock;
char message[BUF_SIZE];
int strLen;
SOCKADDR_IN servAdr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
sock=socket(PF_INET, SOCK_DGRAM, 0);
if(sock==INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));
while(1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
send(sock, message, strlen(message), 0);
strLen=recv(sock, message, sizeof(message)-1, 0);
message[strLen]=0;
printf("Message from server: %s", message);
}
closesocket(sock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
本文章使用limfx的vscode插件快速发布