第六章:基于UDP的服务器端/客户端
上面的第四章和第五章学习了TCP相关的东西。东西比较多,这里学习一下UDP。
6.1 理解IDP
在TCP/ IP协议栈中(在前面的文章中),上面第二层传输层分为TCP和UDP这两种方法。
6.1.1 UDP套接字特点
之前说过,UDP是面向消息的套接字,是不可靠的传输方式
这个过程类似邮递信件,我们无法确认对方是否收到。
这样说似乎TCP更好啊,为什么UDP还有存在的必要呢- 0 -
如果考虑可靠性,TCP确实更胜一筹。
但是UDP在结构上更简洁,UDP不会发送类似ACK的应答消息,也不会像SEQ那样给数据包分配序号,因此性能好很多,编程也比较简单,速度也会更快
虽然可靠性比不上TCP,但也不会像想象中那样频繁数据损毁。
因此,TCP与UDP在本质上的差异在于 流控制机制 。
6.1.2 UDP内部工作原理
从上图看出,IP作用是让离开主机B的UDP数据包准确传递到主机A,
但是UDP包最终嫁给主机A的某一UDP套接字的过程是由UDP完成的。
UDP最重要的作用就是根据端口号,将传到主机的数据包交付给最终的UDP套接字。
6.1.3 UDP的高效使用
若要传递压缩文件(发送一万个数据包时,只要有一个丢失就会产生问题)必须使用TCP
若传递视频或音频时情况有所不同,因为某些情况下需要提供实时服务,所以轻微的画面抖动和杂音是可以接收的,这是速度成了关键因素,这是可以考虑UDP。
TCP慢于UDP的原因通常为以下两点:
- 收发数据前后进行的连接设置及清除过程
- 收发数据过程中为保证可靠性而添加的流控制
因此,如果是频繁需要连接的数据量较小的情况下,UDP比TCP更加高效。
6.2 实现基于UDP的服务器端/客户端
6.2.1 UDP中的服务器和客户端没有连接
UDP服务器和客户端不像TCP那样在连接状态下交换数据,因此无需进行连接过程
也就是说不用调用 TCP连接过程中的 listen/accept 函数。 UDP中只有创建套接字的过程和数据交换的过程
6.2.2 UDP服务器端和客户端只需要一个套接字
TCP中,套接字之间是一一对应的关系,UDP没有!!!!!!!!
(在TCP中,服务器端每次accept时都会自动生成一个连接着客户端的新的套接字+描述符,就是accept函数的返回值,之前声明的套接字作为看大门的守护着listen创建出来的等待序列。)
不管是服务器端还是客户端,都只需要1个套接字。
图中展示了一个UDP套接字与两个不同主机交换数据的过程。也就是说,只需要一个UDP套接字就能和多台主机通信。
6.2.3 基于UDP的数据I/O函数
创建TCP套接字后,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方的套接字连接,TCP套接字知道目标地址信息。
但UDP套接字不会保持连接状态(UDP套接字只有简单的邮筒功能),每次传输数据要添加目标地址的信息,这相当于寄信前在信件中填写地址信息。 下面是填写地址并传输数据时调用的UDP函数。
#include <sys/socket.h>
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr* to,socklen_t addrlen);
-> 成功是返回传输的字节数,失败时返回-1
sock: 用于传输数据的UPD套接字文件描述符
buff: 保存待传输数据的缓冲地址值
nbytes:待传输的数据长度,以字节为单位
flags: 可选项参数,如没有则传递 0
to: 存有目标地址信息的 sockaddr 结构体变量的地址值
addrlen:传递给参数to的地址值结构体变量长度
sendto() 函数与之前的tcp输出函数最大的区别是:此函数需要向他传递目标地址。
下面是UDP接收数据的函数。
UDP数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是同时返回DUP数据包中的发送端信息。
#include <sys/socket.h>
ssize_t recvfrom(int sock,void *buff, size_t nbytes,int flags,struct sockaddr* from, socklen_t* addrlen);
->成功时返回接收的字节数,失败时返回-1
sock: 用于接收数据的UPD套接字文件描述符
buff: 保存接收数据的缓冲地址值
nbytes:可接收的最大字节数,故无法超过参数buff所指的缓冲大小
flags: 可选项参数,如没有则传递 0
to: 存有发送端地址信息的 sockaddr 结构体变量的地址值
addrlen:保存参数from的结构体变量长度的地址值
6.2.4 基于UDP的回声服务器服务器端/客户端
需要注意:不存在请求连接和受理过程,因此在某种意义是上无法明确区分服务器端和客户端。只是因其提供服务而成为服务器端。
uecho_server.c
#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];
struct sockaddr_in serv_addr;
struct sockaddr_in client_addr;
int str_len;
socklen_t client_addr_size;
// 判断一下参数对不对
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_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
// bind 给套接字分配地址信息。
if(bind(serv_sock,(struct sockaddr*)& serv_addr,sizeof(serv_addr)) == -1){
error_handling("bind() error");
}
// 下面是UDP传输过程,这里是无限循环不会结束,除非手动。
while(1)
{
client_addr_size = sizeof(client_addr);
// serv_sock recv message 并把对方的地址信息,存到 client_addr中
str_len = recvfrom(serv_sock,message,BUF_SIZE,0,(struct sockaddr*)&client_addr,&client_addr_size);
// serv_sock send message to client_addr 目标地址
sendto(serv_sock,message,str_len,0,(struct sockaddr*)&client_addr,client_addr_size);
}
close(serv_sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
接下来是与上面的服务器端协同工作的客户端。 区别在于没有connect连接
uecho_client.c
#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 serv_addr;
struct sockaddr_in from_addr;
int str_len;
socklen_t from_addr_size;
// 判断一下参数对不对
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("UDP socket creation error");
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
// 下面是UDP传输过程,这里是无限循环不会结束,除非手动。
while(1)
{
fputs("请输入数据:",stdout);
fgets(message,sizeof(message),stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
// sock send message to 目标地址
sendto(sock ,message ,strlen(message) ,0,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
// sock recv message from from_addr
from_addr_size = sizeof(from_addr);
str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&from_addr,&from_addr_size);
message[str_len] = 0;
printf("message from server:%s", message);
// 理论上讲 serv_addr 与 from_addr 应该是一样的
printf("serv_addr:%x\n", serv_addr.sin_addr.s_addr);
printf("from_addr:%x\n", from_addr.sin_addr.s_addr);
}
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
测试结果如下:
6.2.5 UDP客户端套接字的地址分配
代码中并没有 为客户端套接字分配 IP地址和端口号的过程。
TCP中通过connect函数自动完成,UDP中在sendto函数中自动分配IP和端口号。
sendto(sock ,message ,strlen(message) ,0,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
当然,我们可以在sendto之前调用bind 函数进行地址分配,不分配也是可以的自动分配的信息保存到程序结束~
6.3 UDP的数据传输特性和调用connect函数
之前两章通过示例验证了TCP传输的数据不存在数据边界,本节验证UDP数据传输中存在数据边界。最后讨论UDP中connect函数的调用。
6.3.1 存在数据边界的UDP套接字
不存在数据边界时,数据传输过程中调用I/O函数的次数不具有任何意义
而UDP是具有数据边界的协议,传输中调用I/O函数的次数很重要。
输入函数与输出函数的度奥用次数完全一致,这样才能保证全部已发送数据。
例如:调用3尺输出函数发送的数据必须通过调用3次输入函数才能接收完。
下面上代码,看看效果。
首先是接收端的代码:
接收端需要接收对方的数据,在调用recvfrom函数的时候,会把对方的通信地址存储起来,我们在接收端需要初始化的是 本机的通信地址信息
bound_host1.c
// host1 receive
#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_addr,your_addr;
socklen_t your_addr_size;
int str_len;
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_addr,0,sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
my_addr.sin_port = htons(atoi(argv[1]));
if(bind(sock,(struct sockaddr*)&my_addr,sizeof(my_addr)) == -1){
error_handling("bind() error");
}
fputs("bind() OK!\n",stdout);
for(int i = 0 ; i < 3;i++)
{
sleep(5); //delay 5 sec
your_addr_size = sizeof(your_addr);
str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&your_addr,&your_addr_size);
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);
}
接着是发送端,发送端通过调用sendto函数向接收端发送数据包,需要目标的通信地址信息,因此需要在这部分代码中初始化对方的地址信息。本机的地址信息不需要初始化,在调用sendto函数时会将本机的通信地址发送给接收端,并在接收端保存起来。
bound_host2.c
// host 2 send
#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 am another UDP host!";
char msg3[] = "Nice to meet you!";
struct sockaddr_in your_addr;
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_addr,0,sizeof(your_addr));
your_addr.sin_family = AF_INET;
your_addr.sin_addr.s_addr = inet_addr(argv[1]);
your_addr.sin_port = htons(atoi(argv[2]));
sendto(sock,msg1,sizeof(msg1),0,(struct sockaddr*)&your_addr,sizeof(your_addr));
sendto(sock,msg2,sizeof(msg2),0,(struct sockaddr*)&your_addr,sizeof(your_addr));
sendto(sock,msg3,sizeof(msg3),0,(struct sockaddr*)&your_addr,sizeof(your_addr));
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
这里是测试结果~ 每隔5s 左侧的接收端打印一次message
分析:如果是TCP程序的话,左侧会一次接收3个数据,而这里的UDP是分三次recvfrom出来的。
6.3.2 已连接(connected)UDP套接字与未连接(unconnected)UDP套接字
UDP在传输数据的过程大致分为下面3步:
1. 向UDP套接字注册目标IP和端口号
2. 传输数据
3. 删除DIP套接字中注册的目标地址
每次调用sendto函数可以变更其中的目标IP和端口号,因此可以重复用同一UDP套接字向不同目标传输数据。
与TCP不同的是,TCP需要注册 待传输数据的 目标IP和端口号。
(上面这句话感觉理解起来有点费劲,意思就是TCP 客户端程序在调用connect函数时候,客户端套接字与目标套接字(服务器端套接字)建立了联系,产生了一对一的关系,现在调用write函数向客户端套接字写入任何东西,都会传到对应的 服务器端。
而UDP不一样呀,我并没有建立一对一的对应关系,我每次传输数据和建立连接使用了一个函数 sendto’,我只要改变里面的参数,就能和任何人建立连接并传输数据。)
上面括号里的是我的理解,这样理解之后,我们可以定义未连接套接字和连接套接字。
未注册目标地址的套接字称为未连接套接字。
注册了目标地址的套接字称为连接套接字。
显然,默认情况下UDP属于未连接套接字,TCP属于连接套接字。
6.3.3 创建已连接UDP套接字
细心的朋友发现,我上面说的是默认情况下,证明这是可以改变的!
考虑一下:加入我们向同一个IP和端口号的主机传输3个数据包,使用UDP的话我们需要调用3次sendto函数,
上面看到sendto函数有3步,这将浪费大量时间在 第一步和第三步上面。
如果我们就是想用UDP来传输! 怎么办!
可以利用connect函数 创建已连接的UDP套接字。
sock = socket(PF_INET,SOCK_DGRAM,0);
memset(&adr,0,sizeof(adr));
adr.sin_family = AF_INET;
,,,
,,,
connect(sock,(struct sockaddr*)&adr,sizeof(adr));
看起来似乎和TCP套接字的穿件过程没啥区别啊。除了socket的第二个参数不一样。
当然,使用了connect函数并不意味着要与对方的UDP套接字连接,这只是向UDP套接字注册目标IP和端口信息。
只有与TCP一样,每次调用sendto函数只需要传输数据,因为已经指定了收发对象,所以不仅可以使用sendto
redvfrom函数,也可以使用write函数和read函数进行通信。
下面将之前的回声客户端改成基于UDP套接字的程序。
uecho_con_client.c
#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; // 不再需要 from_adr
if(argc != 3){
printf("Usage %s <IP> <port>\n", argv[0]);
exit(1);
}
// 为客户端分配套接字
sock = socket(PF_INET, SOCK_STREAM, 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]));
if(connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1){
error_handling("connect() error");
}
else{
puts("Connected...... ");
}
while(1)
{
fputs("Input 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_SIZR,0,(struct sockaddr*)&from_adr,&adr_sz);
*/
str_len = read(sock,message,BUF_SIZE - 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);
}
这里只是把sendto变成了write,把recvfrom变成了read,没有任何区别。
服务器端不需要更改。
结果可以参见前文
chapter four 基于TCP的服务器端/客户端(1)