第6章 基于UDP的服务器端和客户端

6.1 理解UDP

1.UDP套接字的特点

通俗的理解,在不考虑传输速率的情况下,可以把UDP理解为信箱,寄信前信封上写上寄信人和收信人地址,之后贴上邮票就可以,我们无法知道对方是否收到。信件丢失也不知道,传输方式不可靠。

只考虑可靠性,TCP确实比UDP好。但是UDP结构上比TCP简洁,而且不会发送类似ACK的应答消息,也不会给数据包分配序号。所以有时UDP性能比TCP高出很多。编程中实现UDP也比TCP简单。UDP可靠性虽然比不上TCP,但也不会像想象中的频繁发生数据损毁。在重视性能的情况下UDP比TCP更好。

为了提供可靠的数据传输服务,TCP在不可靠的IP层进行流控制,而UDP缺少这种流控制机制。流控制是区分UDP和TCP的重要标志,TCP与对方套接字建立连接和断开连接过程也属于流控制的一部分。

TCP的速度无法超过UDP,但是在收发某些类型的数据时可能接近UDP。
每次交换的数据量越大,TCP传输速度就越接近UDP传输速度。

2.UDP内部工作原理

IP的作用是让离开主机B的UDP数据包准确传输到主机A。但是把UDP包最终交给某主机A的某一UDP套接字的过程是UDP完成的。UDP最重要的作用是根据端口号将传到主机的数据包交付给最终的UDP套接字。

3.UDP的高效使用

假设要传输压缩文件,如果传输过程丢失一小部分就可能很难解压,这时就需要TCP连接方式,但是像视频和音频这种数据就算丢失一部分数据,顶多就是卡顿,这时速度要求更多一下,如果还进行流控制,显得很多余这时候就用UDP。

UDP并非每次都快于TCP。TCP比UDP慢的原因

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

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

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

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

UDP服务器/客户端不像TCP那样在连接状态下交换数据,因此和TCP不同,无需经过连接过程。不需要调用accept和listen函数。UDP只需创建套接字和数据交换。

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

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

3.基于UDP的数据I/O函数

创建好TCP之后,传输数据时无需再添加地址信息。因为TCP套接字将保持与对方的连接。TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,因此每次传输数据都要添加目标地址信息。

下面是填写地址并传输数据时调用的UDP函数

#include <sys/socket>
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
成功返回传输的字节数,失败返回-1
sock    用于传输数据的UDP套接字文件描述符
buff    保存待传输数据的缓冲地址值
nbytes  待传输的数据长度,以字节为单位
flags   可选项参数,若没有传递0
to      存有目标地址信息的sockaddr结构体变量的地址值
addrlen 传递给参数to的地址值结构体变量长度

与TCP输出函数最大区别在于,这个函数需要向他传递目标地址信息。

UDP数据发送端并不固定,因此该函数定义为可接受发送端信息的形式,也就是将同时返回UDP数据包中的发送端信息。

#include<sys/socket.h>
ssize_t recvfrom(int sock,void *buff,size_t nbytes ,int flags,struct sockaddr *from,socklen_t *addrlen);
成功返回接收的字节数,失败返回-1
sock    用于接收数据的UDP套接字文件描述符
buff    保存接收数据的缓冲地址值
nbytes  可接受的最大字节数,所以无法超过参数buff所指的缓冲大小
flags   可选参数,若没有传入0
from    存有发送端信息的sockaddr结构体变量的地址值
addrlen 保存存有参数from结构体变量长度的变量地址值

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

UDP不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法区分服务端和客户端。只是因为其提供服务所以称为服务器端。 服务端

#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);//创建UDP套接字参数为SOCK_DGRAM
	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 <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);
}

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

仔细观察UDP客户端,会发现缺少分配IP地址和端口给套接字的过程。TCP客户端是通过connect自动完成这个步骤,那么UDP啥时候分配IP和端口号呢?

UDP程序中,在调用sento函数之前就要完成套接字的地址分配工作,因此调用bind函数。bind不区分TCP和UDP,也就是说UDP中也可以调用。另外代用sendto函数如果尚未分配地址信息,则首次调用sendto函数时,会给相应套接字自动分配IP地址和端口号。此时分配的地址会一直保留到程序运行结束,因此可用来与其他UDP套接字的数据交换。IP用主机IP,端口号用尚未使用的端口号。 总之调用sendto函数会自动分配IP地址和端口号,因此UDP客户端中通常无需额外的地址分配。

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

1.存在数据边界的UDP套接字

UDP是有数据边界的协议,传输中调用I/O函数次数很重要。输入函数的调用次数应该和输出函数的调用次数完全一致,这样才能保证接收全部的已发送的数据。

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

上面代码每次for循环都间隔了5秒

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

recvfrom函数调用间隔为5秒,因此调用recvfrom函数前已经调用了3次shento函数。数据已经传输到了host1.如果是TCP,这时候调用一次read函数就能接收全部数据。UDP不同,这种情况下也需要调用3次recvfrom函数。

UDP套接字传输的数据包又称为数据报,实际上数据报也属于数据报的一种。只是与TCP包不同,其本身可以成为1个完整的数据。
这与UDP的数据传输特性有关,UDP存在数据边界,1个数据包即可成为一个完整的数据,因此称为数据报。

2.已连接(connected)UDP套接字和(unconnected)UDP套接字

TCP套接字中需要注册待传输数据的目标IP和端口号,而UDP中则无需注册,通过sendto函数传输数据过程大体为三个阶段:

  • 第一阶段: 向UDP套接字注册目标IP和端口号

  • 第二阶段: 传输数据

  • 第三阶段: 删除UDP套接字中注册的目标地址信息

每次调用sendto函数重复上述过程。每次都更换目标地址,因此可重复利用UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,就是注册了目标地址信息的套接字称为已连接套接字。显然默认情况下UDP套接字属于未连接套接字。

但是如果与同一个主机进行长时间通信,缩短第一阶段和第三阶段的时间将大大提高性能。

3.创建已连接UDP套接字

只需针对UDP套接字调用connect函数

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

调用connect函数不代表要与对方建立连接,只是为了注册目标IP地址和端口号

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

6.4基于Windows的实现

#include<winsock2.h>
int sendto(SOCKET s,const char* buf,int len,int flags,const struct sockaddr* to,int tolen);

成功返回传输的字节数,失败返回SOCK_ERROR
#include<winsock2.h>
int recvfrom(SOCKET s,char * buf,int len,int flag,struct sockaddr * from,int *fromlen);
成功返回接收的字节数,失败返回SOCK_ERROR

服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET servSock;
	char message[BUF_SIZE];
	int strLen;
	int clntAdrSz;
	
	SOCKADDR_IN servAdr, clntAdr;
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 
	
	servSock=socket(PF_INET, SOCK_DGRAM, 0);
	if(servSock==INVALID_SOCKET)
		ErrorHandling("UDP socket creation error");
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
	servAdr.sin_port=htons(atoi(argv[1]));
	
	if(bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
		ErrorHandling("bind() error");
	
	while(1) 
	{
		clntAdrSz=sizeof(clntAdr);
		strLen=recvfrom(servSock, message, BUF_SIZE, 0, 
								(SOCKADDR*)&clntAdr, &clntAdrSz);
		sendto(servSock, message, strLen, 0, 
								(SOCKADDR*)&clntAdr, sizeof(clntAdr));
	}	
	closesocket(servSock);
	WSACleanup();
	return 0;
}

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

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET sock;
	char message[BUF_SIZE];
	int strLen;
	
	SOCKADDR_IN servAdr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
		ErrorHandling("WSAStartup() error!"); 

	sock=socket(PF_INET, SOCK_DGRAM, 0);   
	if(sock==INVALID_SOCKET)
		ErrorHandling("socket() error");
	
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family=AF_INET;
	servAdr.sin_addr.s_addr=inet_addr(argv[1]);
	servAdr.sin_port=htons(atoi(argv[2]));
	
	connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));

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

		send(sock, message, strlen(message), 0);
		strLen=recv(sock, message, sizeof(message)-1, 0);

		message[strLen]=0;
		printf("Message from server: %s", message);
	}	
	closesocket(sock);
	WSACleanup();
	return 0;
}

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


本文章使用limfx的vscode插件快速发布