TCP/IP网络编程C06-基于UDP的服务器端⁄客户端

学习笔记

理解UDP

UDP在结构上比TCP简洁,在更注重性能而非可靠性的情况下,UDP是一种很好的选择。

流控制是区分UDP与TCP最重要的标志。UDP也并非总是快于TCP,TCP比UDP慢的原因通常为以下两点:

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

如果收发的数据量小但需要频繁连接,UDP比TCP高效

UDP中服务端与客户端间没有连接

因为 UDP 是无连接的,所以在编程时不需要调用 listen 函数和 accept 函数。UDP中只有创建套接字的过程和数据交换的过程

UDP不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端与客户端

UDP服务端与客户端均只需一个套接字

TCP 中,服务器端和客户端的套接字是一对一的关系,服务器端每向一个客户端提供服务,就需要分配一个新的套接字。

而 UDP 的服务器端和客户端均只需 1 个套接字,服务器端只要有一个 UDP 套接字就可以和多台主机通信

TCP 中,套接字之间应该是一对一的关系。若要向 10 个客户端提供服务,除了守门的服务器套接字之外,还需要 10 个服务器套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。只需要一个 UDP 套接字就可以向任意主机传输数据,如图所示

基于UDP的数据I/O函数

UDP 套接字不会保持连接状态,因此每次传输数据时都要添加目标地址信息(相当于寄信前在信封上写收信地址)

发送 UDP 数据的函数

#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes,
           int flags, struct sockaddr *to,socklen_t addrlen);

// 功能:向 to 中所指明的目标地址发送数据。
// 参数:sock:UDP 套接字文件描述符; buff:待传输的数据; nbytes:待传输的数据长度(单位是字节); 
// flags:可选项参数,没有则为 0; to:包含目标地址信息; addrlen:包含目标地址信息的结构体变量的长度
// 返回值:成功时返回接收的字节数,失败时返回 -1。

接收 UDP 数据的函数

接收端本来是不知道发送端的地址的,但调用完 recvfrom 函数后,发送端的地址信息就会存储到参数 from 指向的结构体中。

#include <sys/socket.h>
ssize_t recvfrom(int sock, void *buff, size_t nbytes,
                int flags, struct sockaddr *from, socklen_t *addrlen);

// 功能:接收发送端信息
// 参数:sock:UDP 套接字文件描述符; buff:用户保存接收的数据;nbytes:可接收的最大字节数;  
// flags:可选项参数,没有则为 0; from:用来存储发送端的地址信息; addrlen:包含发送端地址信息的结构体变量的长度
// 返回值:成功时返回传输的字节数,失败时返回 -1。 

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

在 TCP 的客户端中 conncect 函数会自动完成给套接字分配源 IP 地址和端口号的过程,UDP 中则是 sendto 函数来完成此功能。

如果调用 sendto 函数时发现尚未给套接字分配地址信息,就会在首次调用 sendto 函数时给套接字分配源 IP 地址和端口。

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

UDP套接字存在数据边界

对于UDP,传输中的I/O函数的调用次数非常重要。输入函数与输出函数的调用次数应完全一致,才能保证全部接收已发数据。

已连接UDP套接字与未连接UDP套接字

通过 sendto 函数传输数据的过程包括三个阶段:

  1. 向 UDP 套接字注册目标 IP 和端口号;(注意:是将 UDP 套接字与目标的地址信息相关联,不是给 UDP 分配源地址信息。前者每次 sendto 都会执行,后者只有首次调用且套接字尚未分配地址时才会执行一次)。

  2. 传输数据;

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

当多次通过 sendto 向同一个目标发送信息时,每次 sendto 都进行上面的步骤 1 和 3,就会很浪费时间。

因此当要长时间与同一主机通信时,将 UDP 变为已连接套接字会提高效率。

未连接套接字:未注册目标地址信息的套接字(UDP默认)

已连接套接字:注册了目标地址信息的套接字(使用connect()connect()作用只是为了注册目标地址信息),在需要长时间通信时,使用已连接UDP套接字更高效

创建已连接套接字

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));// 注意:adr 是目标的地址信息
//使用已连接的 UDP 套接字进行通信时, sendto 函数就不会再执行步骤 1 和步骤 3,每次只要传输数据即可。
//并且已连接的 UDP 套接字也可以通过 write、read 函数进行通信。

习题答案

Q01

  1. UDP 为什么比 TCP 速度快?为什么 TCP 数据传输可靠而 UDP 数据传输不可靠?

    UDP 和 TCP 不同,不进行流量控制。由于该控制涉及到套接字的连接和结束,以及整个数据收发过程,因此,TCP 传输的数据是可以信赖的。相反, UDP 不进行这种控制,因此无法信任数据的传输,但正因 UDP 不进行流量控制,所以比 TCP 更快

Q02

  1. 下面不属于 UDP 特点的是?

    下面加粗的代表此句话属于 UDP 特点

    a. UDP 不同于 TCP ,不存在连接概念,所以不像 TCP 那样只能进行一对一的数据传输。

    b. 利用 UDP 传输数据时,如果有 2 个目标,则需要 2 个套接字。(每次调用 sendto()函数时都需要重新设定目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。)

    e. UDP 套接字中无法使用已分配给 TCP 的同一端口号

    d. UDP 套接字和 TCP 套接字可以共存。若需要,可以同时在同一主机进行 TCP 和 UDP 数据传输。

    e. 针对 UDP 函数也可以调用 connect 函数,此时 UDP 套接字跟 TCP 套接字相同,也需要经过 3 次握手阶段。

Q03

  1. UDP 数据包向对方主机的 UDP 套接字传递过程中,IP 和 UDP 分别负责哪些部分?

    IP 负责链路选择。UDP 负责端到端的传输(根据端口号将传到主机的数据包交付给最终的 UDP 套接字)

Q04

  1. UDP 一般比 TCP 快,但根据交换数据的特点,其差异可大可小。请说明何种情况下 UDP 的性能优于 TCP

    UDP 与 TCP 不同,不经过连接以及断开 SOCKET 的过程,因此,在收发数据量小,频繁的连接及断开的情况下,UDP 的数据收发能力会凸显出更好的性能。

Q05

  1. 客户端 TCP 套接字调用 connect 函数时自动分配 IP 和端口号。UDP 中不调用 bind 函数,那何时分配 IP 和端口号?

    首次调用 sendto 函数时,发现尚未分配信息,则给相应的套接字自动分配源 IP 和端口号,而且此时分配的地址一直保留到程序结束为止。(源地址和端口不会变,目的地址和端口可能会不断改变)

Q06

  1. TCP 客户端必须调用 connect 函数,而 UDP 中可以选择性调用。请问,在 UDP 中调用 connect 函数有哪些好处?

    每当以 UDP 套接字为对像调用 sendto 函数时,都要经过以下过程

    • 第一阶段:为目标 UDP 注册端口和 IP
    • 第二阶段:数据传输
    • 第三阶段:删除 UDP 注册的 IP 和端口信息

    其中,只要调用 connect 函数,就可以忽略每次传输数据时反复进行的第一阶段和第三阶段。然而,调用 connect 函数并不意味着经过连接过程,只是将 IP 地址和端口号指定在 UDP 的发送对象上。这样 connect 函数使用后,还可以用 write、read 函数进行数据处理,而不必使用 sendto、recvfrom。

    每次调用 sendto()函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接 connected套接字。显然,UDP套接字默认属于未连接套接字。

    因为三个阶段中,第一个阶段和第三个阶段占用了一大部分时间,调用 connect 函数可以节省这些时间。

Q07

  1. 请参考本章给出的 echo_server.c和 echo_client c,编写示例使服务器端和客户端轮流收发消息。收发的消息均要输出到控制台窗口。
/********************************uchar_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];
	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);
	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");

	clnt_adr_sz=sizeof(clnt_adr);
	while(1) 
	{
		str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
								(struct sockaddr*)&clnt_adr, &clnt_adr_sz);//收信息
		message[str_len]=0;
		printf("Message from client: %s", message);

		fputs("Insert message(q to quit): ", stdout);
		fgets(message, sizeof(message), stdin);
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))	
			break;

		sendto(serv_sock, message, strlen(message), 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);
}


/********************************uchar_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;
	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);
}

书本源码

01-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];
	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);
	if(serv_sock==-1)
		error_handling("UDP socket creation error");
	

	//填写服务器的IP和端口号
	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)//bind函数不区分UDP和TCP
		error_handling("bind() error");

	while(1) 
	{
		clnt_adr_sz=sizeof(clnt_adr);

	
	/*
	ssize_t recvfrom(int sock, void *buff, size_t nbytes,
                int flags, struct sockaddr *from, socklen_t *addrlen);

	// 功能:接收发送端信息
	// 参数:sock:UDP 套接字文件描述符; buff:用户保存接收的数据;nbytes:可接收的最大字节数;  
	// flags:可选项参数,没有则为 0; from:用来存储发送端的地址信息; addrlen:包含发送端地址信息的结构体变量的长度
	// 返回值:成功时返回传输的字节数,失败时返回 -1。
	*/
		str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
								(struct sockaddr*)&clnt_adr, &clnt_adr_sz);



	/*
	
	ssize_t sendto(int sock, void *buff, size_t nbytes,
           int flags, struct sockaddr *to,socklen_t addrlen);

	// 功能:向 to 中所指明的目标地址发送数据。
	// 参数:sock:UDP 套接字文件描述符; buff:待传输的数据; nbytes:待传输的数据长度(单位是字节); 
	// flags:可选项参数,没有则为 0; to:包含目标地址信息; addrlen:包含目标地址信息的结构体变量的长度
	// 返回值:成功时返回接收的字节数,失败时返回 -1。
	
	*/	

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

/******************** input******************
*description:

TCP中,套接字之间应该是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。
但在UDP中,不管是服务器端还是客户端都只需要1个套接字。

*content:

./01-uecho_server 9190

*******************************************/



/******************** output******************
*description:




*content:



*******************************************/

02-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];
	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));//如果调用 sendto函数时发现尚未分配地址信息,则在首次调用 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;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

/******************** input******************
*description:



*content:
/02-uecho_client 127.0.0.1 9190


*******************************************/



/******************** output******************
*description:




*content:

Insert message(q to quit): hello
Message from server: hello
Insert message(q to quit): how are you
Message from server: how are you
Insert message(q to quit): q

*******************************************/

03-bound_host1.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 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");
	
	//填写服务器的IP和端口号
	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);
}

/******************** input******************
*description:

验证UDP接收数据是有边界的,并且可能是乱序的

*content:

./03-bound_host1 9190


*******************************************/



/******************** output******************
*description:




*content:
Message 1: Hi! 
(休息5s.....)
Message 2: I'm another UDP host! 
(休息5s.....)
Message 3: Nice to meet you


*******************************************/

04-bound_host2.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 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");
	

	//设置目的端口的IP和端口
	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);
}

/******************** input******************
*description:

验证UDP接收数据是有边界的

*content:

./04-bound_host2 127.0.0.1 9190

*******************************************/



/******************** output******************
*description:




*content:

无


*******************************************/

05-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;
	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]));
	
	//使用已连接的 UDP 套接字进行通信时, sendto 函数就不会再执行步骤 1 和步骤 3,每次只要传输数据即可。
	//并且已连接的 UDP 套接字也可以通过 write、read 函数进行通信。
	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);
}


/******************** input******************
*description:

说明connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))函数在UDP中的作用

*content:



*******************************************/



/******************** output******************
*description:




*content:



*******************************************/

参考链接

posted @ 2021-12-17 21:46  MyBluehat  阅读(65)  评论(0)    收藏  举报