Part 1 start network programming:chapter SIX: 基于UDP的服务器端/客户端

第六章:基于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的原因通常为以下两点:

  1. 收发数据前后进行的连接设置及清除过程
  2. 收发数据过程中为保证可靠性而添加的流控制

因此,如果是频繁需要连接的数据量较小的情况下,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)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值