TCP/IP网络编程 (六):基于UDP的服务器端/客户端

理解UDP

UDP套接字的特点

下面通过信件说明UDP的工作原理。寄信前先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。

无法确认对方是否收到信件,并且在邮寄过程中可能发生信件丢失的情况。也就是说,UDP是不可靠的数据传输服务。


TCP为了提供可靠的数据传输服务,在不可靠的IP层进行流控制,所以TCP比UDP可靠,但UDP在结构上比TCP更简洁。

流控制是区分UDP和TCP的最重要的标志。


UDP内部工作原理

IP的作用就是让离开主机B的UDP数据包准确传递到主机A。UDP最重要的作用就是根据端口号将传递到主机的数据包交付给最终的UDP套接字。


UDP的高效使用

UDP具有一定的可靠性。网络传输特性导致信息丢失频发,可若要传输压缩文件(发送1W个数据包,丢失一个就会产生问题),则必须使用TCP。对于多媒体数据而言,丢失一部分没问题,只会引起短暂的画面抖动,或细微的杂音。

TCP比UDP慢的原因通常有以下两点:

-收发数据前后进行的连接设置及清除过程。

-收发数据过程中为保证可靠性而添加的流控制。


实现基于UDP的服务器端/客户端

UDP中的服务器和客户端没有连接

UDP只有创建套接字的过程和数据交换过程。不必调用TCP连接过程中调用的listen函数和accept函数。


UDP服务器端和客户端均只需1个套接字

TCP中套接字之间是一对一的关系。但在UDP中,不管服务器端还是客户端都只需要一个套接字。

(收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有一个邮筒,就可以通过它向任意地址寄出信件。


下图展示1个UDP套接字和2个不同的主机数据交换的过程:

                        


基于UDP的数据I/O函数

UDP套接字每次传输数据都要添加目标地址信息,相当于寄信前在信件中填写地址。


填写地址并传输数据时调用的函数:


接受UDP数据的函数:



基于UDP的回声服务器端/客户端


服务器端:

/* 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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

int main(int argc,char *argv[])
{
	int serv_sock;
	char message[BUF_SIZE];

	int str_len;

	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t clnt_adr_sz;

	if (argc != 2) {
		printf("Usage : %s <port> \n",argv[0]);
		exit(1);
	}

	serv_sock = socket(PF_INET,SOCK_DGRAM,0);				//创建UDP套接字
	if (serv_sock == -1)
		error_handling("UDP socket() 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);
		//通过47行的函数调用同时获取数据传输端的地址,并将接受的数据传会该地址
	}
		
	close(serv_sock);
	return 0;
}

客户端:

/* UDP回声客户端 */
#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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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("Input message(Q to quit): ",stdout);
		fgets(message,sizeof(message),stdin);

		if (!strcmp(message,"q\n") || !strcmp(message,"Q\n"))		// 输入q/Q退出
			break;

		sendto(sock,message,strlen(message),0,(struct sockaddr*) &serv_adr,sizeof(serv_adr));	
		// 首次sendto函数时给相应套接字自动分配IP和端口,分配的地址一直保存到程序结束为止
		
		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;
}

UDP客户端套接字的地址分配

UDP客户端缺少把IP和端口分配给套接字的过程。TCP客户端是调用connect函数自动完成此过程。那UDP中何时分配IP和端口号?

UDP程序中,调用sendto函数传输数据前完成套接字的地址分配工作,因此调用bind函数。如果调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。而且分配的地址一直保存到程序结束。


综上所述,调用sendto函数时自动分配IP和端口号,UDP客户端无需额外的地址分配过程。


UDP的数据传输特性和调用connect函数

存在数据边界的UDP套接字

UDP是具有数据边界的协议,传输中调用I/O函数的次数非常重要。输入函数的调用次数和输出函数的调用次数完全一致,才能保证接收全部已发送数据。例如调用3次输出函数发送的数据必须调用3次输入函数才能接受完。


示例:

host1(服务器端):

#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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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++ )	//循环3次,接收3次数据
	{
		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;
}

host2(客户端):

#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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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;
}

UDP通信过程中使I/O函数调用次数保持一致


已连接(connected) UDP套接字与为连接UDP套接字

通过sendto函数传输数据的过程大致可分为以下3个阶段:

-向UDP套接字注册目标IP和端口号

-传输数据

-删除UDP套接字中注册的目标地址信息


每次调用sendto函数时重复上述过程,每次都变更目标地址。因此可以重复利用同一UDP套接字向不同目标传输数据。

UDP这种未注册目标地址信息的套接字称为未连接套接字。反之,注册了目标地址的套接字称为连接connected套接字。


当向同一个端口多次传输数据时,就会多次重复以上过程。因此,要与同一主机进行长时间通信时,将UDP套接字变成已连接套接字会提高效率。


创建已连接UDP套接字

创建已连接UDP套接字的过程很简单,针对UDP套接字调用connect函数

sock = socket(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*) &adr, sizeof(adr));

与TCP套接字创建过程一致,只是socket函数第二个参数变为SOCK_DGRAM。

之后就与TCP套接字一样,每次调用sendto函数时只需传输数据,因为已经指定了收发对象。

此时也可以使用write,read函数进行通信。


下面将之前的uecho.client.c改为基于已连接UDP套接字的程序。

/* UDP已连接客户端 */
#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)
{
	fputs(message,stderr);
	fputc('\n',stderr);
	exit(1);
}

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("Input message(Q to quit): ",stdout);
		fgets(message,sizeof(message),stdin);

		if (!strcmp(message,"q\n") || !strcmp(message,"Q\n"))		// 输入q/Q退出
			break;
		/* 
		sendto(sock,message,strlen(message),0,(struct sockaddr*) &serv_adr,sizeof(serv_adr));	
		*/
		
		write(sock,message,strlen(message));			//可以使用write传输数据
		
		/*
		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);	//可以使用read读取数据

		message[str_len] = 0;
		printf("Message from server: %s",message);
	}

	close(sock);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80212981