UDP(User Datagram Protocol)和TCP(Transmission Control Protocol)是两种常用的互联网传输协议,它们在数据传输方面有以下几个主要区别:
连接与无连接:TCP是面向连接的协议,而UDP是无连接的协议。TCP在进行数据传输前会先建立连接,然后进行数据传输,最后断开连接。而UDP在发送数据之前不需要建立连接。
可靠性:TCP提供可靠的数据传输,它使用确认机制和重传机制来确保数据的可靠性,能够检测丢包和重排数据包。UDP则没有这些机制,它不保证数据传输的可靠性,也无法检测丢包。
顺序性:TCP保证数据的顺序性,即发送的数据包按照顺序到达接收端。UDP发送的数据包可能会乱序到达接收端,接收端需要根据需要自行进行排序。
建立连接复杂性:TCP在建立连接时需要进行三次握手,比较复杂,但能够保证连接的可靠性。UDP不需要建立连接,因此建立连接的过程简单。
下面我结合具体情况来分析一下上述区别,对于这个第一点区别实际上就是TCP在连接之前要进行的三次握手和断开的四次挥手,当三握四挥完成后才真正代表一个TCP连接已经连接/断开。但UDP不需要连接,可以理解为它可以直接发送数据。对于第二点区别实际上是TCP的各种机制结合从而得到的一个特性,比如滑动窗口,超时重传,三握四挥,拥堵控制等等机制,而UDP缺少这些机制。第三和第四个区别实际上都是TCP拥有的丰富机制导致的,相比之下UDP没有这么丰富的机制。
但是这些繁杂的机制导致TCP的速度一般比UDP慢,虽然TCP在一些数据可靠性较高的地方发挥作用,UDP也在对实时性要求较高的直播,视频等领域发挥重要的作用。
UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字的过程和数据交换过程。
TCP中,套接字之间应该是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。可以把UDP的原理比作送信,收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有1个邮筒,就可以通过它向任意地址寄出信件。同样,只需1个UDP套接字就可以向任意主机传输数据。
由于TCP在发送数据之前已经将地址绑定在套接字中了,只需要往套接字里面“塞数据”就可以了。但UDP是无连接的,所以每一发送数据的时候都要确定一下发送目的地。就像送信一样,送信之前要确定一下送信的地址。接下来介绍一下UDP的数据传输函数。
#include
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
//成功时返回传输的字节数,失败时返回-1
sock //用于传输数据的UDP套接字
buff //用于保存传输数据的缓冲地址
nbytes //待传输数据的长度,以字节为单位
flags //可选项参数
to //存有目标地址的sockaddr结构体参数
addrlen //传递给参数to的结构体长度
接下来是接受UDP数据的函数
#include
ssize_t recvfrom(int sock,void *buff,size_t nbytes,int flags,struct sockaddr*from,socklen_t addrlen);
//成功时返回接受的字节数,失败时返回-1
sock //用于接受数据的UDP套接字文件描述符
buff //用于保存接受数据的缓冲值地址
nbytes //可接受的最大字节数
flags //可选项参数
from //存有发送端地址的sockaddr结构体参数
addrlen //保存参数from结构体变量长度的地址值
编写UDP程序最核心的部分就在于上述的两个参数。
下面结合之前的内容实现回声服务器。但是注意一下UDP和TCP不一样,因此在某种一样上无法明确区分服务器端和客户端。
以下是服务器端代码:
#include
#include
#include
#include
#include
#include
#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(argcl=2){
printf("Usage : %s \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<"与服务端代码相同,故省略">
#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(argcl=3){
printf("Usage :%s \n", argv[e]);
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,"e\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);
}
在上述的客户端代码中,我们发现客户端的套接字并没有有分配IP地址和端口号的过程,既然如此,UDP客户端何时分配IP地址和端口号。服务器端通过bind函数来分配IP地址和套接字,从而在sendto函数调用之前就分配好了。那么客户端在何时调用呢?这里就直接给出结论了,和TCP套接字的connect自动分配一样,UDP客户端IP地址和端口号分配是在调用sendto函数自动分配的。而且此时分配的地址一直保留到程序结束位置。
综上所述,调用sendto函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的地址分配过程。
UDP在通过sendto函数传输数据时会经过以下三个阶段:
1.向UDP套接字注册目标IP和端口号
2.传输数据
3.删除在UDP套接字中注册的目标地址信息
每次调用sendto函数都会重复上述三个过程的UDP套接字叫做未连接套接字。每次都变更目标地址,因此可以重复利用同一UDP套接字向不同的目标传递数据。但是如果此时想要向一个特定的目标地址长时间传递数据,那么此时过程1和过程2将会占用大量时间,如果可以缩短这部分时间的话,可以大大提高整体性能。
如果想要称为连接套接字的话,只要在前面的基础上调用这个函数即可
connect(sock,(struct sockaddr*)&addr,sizeof(addr));
这里要注意一下,这个所谓调用的connect函数并非想要与服务器套接字连接,它只是将地址信息注册进UDP套接字而已。
之后就和TCP套接字一样,每次调用sendto函数只需传输数据。进一步的,不仅可以使用sendto,recvfrom函数,还可以使用write,read函数进行通信。
这里介绍一下Windows平台的sendto和recvfrom函数。实际上与Linux的函数没有太大区别。
#include
int sendto(SOCKET s,const char* buf,int len,int flags,const struct sockaddr*to,int tolen);
//成功时返回传输的字节数,失败时返回SOCKET_ERROR
int recvfrom(SOCKET s,char*buf,int len,int flag,struct sockaddr*from,int* fromlen);
//成功时返回接受的字节数,失败时返回SOCKET_ERROR