【计算机网络】---数据报套接字通信

引言

数据报套接字。它提供了一种无连接、不可靠的双向数据传输服务。数据包以独立的形式被发送,并且保留了记录边界,不提供可靠性保证。数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据。在TCP/IP协议簇中,使用UDP协议来实现数据报套接字。在出现差错的可能性较小或允许部分传输出错的应用场合,可以使用数据报套接字进行数据传输,这样通信的效率较高。其服务灵活简单,在现实生活中得到了广泛的应用。

TCP传输数据的缺点

在了解UDP协议之前,我们需要先来了解一下TCP协议存在哪些缺陷呢?
相当于UDP协议来说,TCP协议增加了可靠性,流量控制、拥塞控制等机制,能够保证数据传递的可靠性,那么是不是在所有情况下使用TCP协议都是最合适的呢?

  • 首先,使用TCP协议传输数据的代价相对于UDP协议而言要高许多。如果使用TCP协议实现一次请求-应答交换,由于TCP协议使用3次握手建立连接,并且再关闭连接时进行4四次挥手交互,那么最小事务处理时间将是2✖RTT+SPT,其中RTT表示客户与服务器之间的往返时间,STP表示客户请求的服务器处理时间。相比之下,UDP没有连接建立和释放时间,就单个UDP请求-应答交互而言的最小处理时间仅为RTT+SPT,比TCP减少了一个RTT。因此,传输代价是使用TCP协议时必须要考虑的一个损失。
  • 其次,连接的存在意味着连接维护的代价。服务器要为每一个已经建立连接的客户分配单独使用的资源,如用于接收和发送的TCP缓冲区、存储连接相关参数的TCP变量等,这对于有可能胃痛是来自数百个不同客户的请求提供服务的服务器来说,会严重增加该服务器的负担,甚至于造成服务器资源的过耗。
  • 最后,在每个连接的通信过程中,TCP拥塞控制中的慢启动策略会起作用,使得每个TCP连接都要起始于慢启动阶段。由此带来的结果是数据通信的效率不会马上到达TCP的最大传输性能,也因此增大了使用TCP协议进行网络通信的传输延迟。

由此分析来看,尽管TCP提供了可靠的数据传输服务,简化了上层应用程序的设计复杂性,但同时也有一些性能和资源方面的损失,TCP协议未必是所有网络应用程序在选择传输协议时最佳的的选择。

UDP传输特点

UDP协议是一个无连接的传输层协议,提供面向事务的简单、不可靠的信息传送服务。

UDP协议的传输特点表现在以下方面:
多对多通信:UDP在通信实体的数据量上具有更大的灵活性,多个发送方可以向一个接收方发送报文,一个发送方也可以向多个接收方发送数据,更重要的是,UDP能让应用使用底层网络的广播或者组播设施交付报文。

不可靠服务:UDP提供的服务是不可靠交付的,即报文可以丢失、重复或失序,它没有重传设施,如果发生故障,也不会通知发送方。

缺乏流量控制:UDP不提供流量控制,当数据包到达的速度比接收系统或应用的处理速度快时,只是将其丢弃而不会发出警告。

报文模式:UDP提供了面向报文的传输方式,在需要传输数据的时候,发送方准确指明要求发送数据的字节数,UDP将这些数据放置在一个外发送报文中,在接受方,UDP一次交付一个传入报文。因此当有数据交付时,接收到的数据拥有和发送方应用程序所指定的一样的报文边界。

UDP首部

UDP数据报文封装在IP数据包的数据部分,UDP数据在IP数据包中的封装如下图所示:
在这里插入图片描述

(1)UDP首部的数据格式如下

在这里插入图片描述

(2)UDP首部个字段的含义如下
  • 1.源、目的端口号。每个UDP数据的报文都包含源端口号和目的端口号,用于寻找发送端和接收端的应用进程。UDP端口号和TCP端口号是相互独立的。
  • 2.UDP长度。UDP长度字段指UDP首部和UDP数据的字节总长度,该字段的最小值是8,即数据部分位0.
  • 3.UDP校验和。UDP校验和是一个端到端的校验和。它由发送端计算,然后由接收端验证,其目的是发现UDP首部和数据在发送端到接收端之间发生的任何变动。检验和的覆盖范围包括UDP首部、UDP伪首部和UDP数据。UDP的校验和是可选的,如果UDP中校验和字段为0,表示不进行校验和计算。
(3)数据报套接字编程的适用场合

数据报套接字基于不可靠的报文传输服务,这种服务的特点是无连接、不可靠。无连接的特点决定了数据报套接字的传输非常灵活,具有资源消耗小、处理速度快的优点。而不可靠的特点意味着在网络质量不佳的环境下,发生数据包丢失的现象会比较严重,因此上层应用程序在设计开发时需要考虑网络应用程序运行的环境以及数据在传输过程中的丢失、乱序、重复对应用程序带来的负面影响。总体来看,数据报套接字适合于在以下场合使用:

1)音频、视频的实时传输应用。数据报套接字适合用于音频、视频这类对实时性要求比较高的数据传输应用。传输内容通常被切分为独立的数据报,其类型多为编码后的媒体信息。在这种应用场景下,通常要求实时音视频传输,与TCP协议相比,UDP 协议减少了确认、同步等操作,节省了很大的网络开销。UDP协议能够提供高效率的传输服务,实现数据的实时性传输,因此在网络音视频的传输应用中,应用UDP协议的实时性并增加控制功能是较为合理的解决方案,如RTP和RTCP在音视频传输中是两个广泛使用的协议组合,通常RTP基于UDP传输音视频数据,RTCP基于TCP传输提供服务质量的监视与反馈、媒体间同步等功能。
2)广播或多播的传输应用。流式套接字只能用于1对1的数据传输,如果应用程序需要广播或多播传送数据,那么必须使用UDP协议,这类应用包括多媒体系统的的多播或广播业务、局域网聊天室或者以广播形式实现的局域网扫描器等。
3)简单高效需求大于可靠需求的传输应用。尽管UDP不可靠,但其高效的传输特点使其在一些特殊的传输应用中受到欢迎,比如聊天软件常常用到UDP协议传送文件,日志服务器通常设计位基于UDP协议来接受日志。这些应用不希望在每次传递小数据时消耗昂贵的TCP连接建立与维护代价,而且即使偶尔丢失一两个数据包,也不会对接受结果产生大影响,在这种场景下,UDP协议的简单高效特性就会非常的合适。

数据报套接字的通信过程

使用数据报套接字传送数据类似于生活中的邮件发送,与流式套接字的通信过程有所不同,数据报套接字不需要建立连接,而是直接根据目的地址构造数据包进行传送。

(1)基于数据报套接字的服务器进程的通信过程

在通信过程中,服务器进程作为服务提供方,被动接受客户的请求,使用UDP协议与客户交互,其基本通信过程如下:

1) Windows Sockets DLL初始化,协商版本号:
2)创建食接字,指定使用UDP (无连接的传输服务)进行通信:
3)指定本地地址和通信端口;
4)等待客户的数据请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。

(2)基于数据报套接字的客户进程的通信过程

在通信过程中,客户进程作为服务请求方,主动向服务器发送服务器请求,使用UDP协议与服务器交互,其基本通信过程如下:

1) Windows Sockets DLL初始化,协商版本号;
2)创建套接字,指定使用UDP (无连接的传输服务)进行通信;
3)指定服务器地址和通信端口;
4)向服务器发送数据请求;
5)进行数据传输;
6)关闭套接字;
7)结束对Windows Sockets DLL的使用,释放资源。

(3)数据报套接字的使用模式

我们知道,UDP是一个无连接协议,也就是说,它仅仅传输独立的有目的地址的数据报。“连接”的概念似乎与数据报套接字无关,而实际上,在有些情况下,“连接”在数据报套接字中的使用可以帮助网络应用程序在可靠性和效率方面有一一定程度的优化。
1.两种数据报套接字的使用模式
在数据报套接字的使用过程中,可以有两种数据发送和接收的方式。

  • (1)非连接模式
    在非连接模式下,应用程序在每次数据发送前指定目的IP和端口号,然后调用sendto(函数或WSASendToO函数将数据发送出去,并在数据接收时调用recvfrom0函数或WSARecvFrom()函数,从函数返回参数中读取接收数据报的来源地址。这种模式通常适用于服务器的设计,服务器面向大量客户,接收不同客户的服务请求,并将数据应答发送给不同的客户地址。另外,这种模式也同样适用于广播地址或多播地址的发送。以广播方式发送数据为例,应用程序需要使用setsockopt()函数来开启SO_BROADCAST选项,并将目的地址设置为INADDR_ BROADCAST (相当于inet addr(“255.255.255.255" ))。

非连接模式是数据报套接字默认使用的数据发送和接收方式,这种模式的优点是数据发送的灵活性较好。

  • (2)连接模式
    在连接模式下,应用程序首先调用connect()函数或WSAConnect()函数指明远端地址,即确定了唯一的通信对方地址,在之后的数据发送和接收过程中,不用每次重复指明远程地址就可以发送和接收报文。此时,send()函数、WSASend( )函数、sendto()函数和WSASendTo()函数可以通用,recv()函数、WSARecv()函数、recvfrom()函数和WSARecvFrom()函数也可以通用。处于连接模式的数据报套接字工作过程如图6-5所示,来自其他不匹配的IP地址或端口的数据报不会投递给这个已连接的套接字。如果没有相匹配的其他套接字,UDP将丢弃它们并生成相应的ICMP端口不可达错误。
(4)"连接"在套接字中的含义

对于TCP来说,调用connect0将导致双方进人TCP的三次握手初始化连接阶段,客户会发送SYN段给服务器,接收服务器返回的确认和同步请求,在连接建立好后,双方交换了一些初始的状态信息,包括双方的IP地址和端口号。因此,对于流式套接字的connect()函数操作而言,connect() 函数完成的功能是: 1) 在调用方为套接字关联远程主机的地址和端口号; 2)与远端主机建立连接。该函数的成功暗示着服务器是正在提供服务的且双方的路径是可达的。从使用次数上来看,connect() 函数只能在流式套接字上调用次。

对于UDP来说,由于双方没有共享状态要交换,所以调用connect()函数完全是本地操作,不会产生任何网络数据。因此,对于数据报套接字的connect()操作而言,conect)函数完成的功能是:在调用方为套接字关联远程主机的地址和端口号。由于没有网络通信行为发生,该函数的成功并不意味着对等方- -定会对后续的数据请求产生回应,可能服务器是关闭的,也可能网络根本就没用连通。也可能网络根本就没有连通。一个 数据报套接字可以多次调用cnnect()函数,目的可能是: 1)指定新的IP地址和端口号; 2)断开套接字。对于第一个目的,通过再次调用connect(), 可以使得数据报套接字更新所关联的远端端点地址;对于第二个目的,为了断开一个已连接的数据报套接字,在再次调用connect()函数时,把套接字地址结构的地址族成品设置为AF_UNSPEC,此时,后续的send()/WSASend()、recvO/WSARecv()函数都将返回错误。

数据报通信代码如下

客户端:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv)
{
	WSADATA wsaData;
	SOCKET ConnectLessSocket = INVALID_SOCKET;
	struct addrinfo *result = NULL, *ptr = NULL, hints;
	char *sendbuf = "this is a test";
	char recvbuf[DEFAULT_BUFLEN];
	int iResult;
	int recvbuflen = DEFAULT_BUFLEN;
	if (argc != 2) {
		printf("usage: %s server-name\n", argv[0]);
		return 1;
	}

	//初始化套接字
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0) {
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}
	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;

	//解析服务器地址和端口号
	iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
	if (iResult != 0) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}
	//创建数据报套接字
	ConnectLessSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
	if (ConnectLessSocket == INVALID_SOCKET) {
		printf("scoket failed with error: %ld\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	//发送缓冲区中的测试数据
	iResult = sendto(ConnectLessSocket, sendbuf, (int)strlen(sendbuf), 0, result->ai_addr, (int)result->ai_addrlen);
	if (iResult == SOCKET_ERROR) {
		printf("send failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectLessSocket);
		WSACleanup();
		return 1;
	}
	freeaddrinfo(result);
	printf("Bytes Sent: %ld\n", iResult);

	//接收数据 
	iResult = recvfrom(ConnectLessSocket, recvbuf, recvbuflen, 0, NULL, NULL);
	if (iResult > 0)
		printf("Bytes received: %d\n", iResult);
	else if (iResult == 0)
		printf("Connection closed\n");
	else
		printf("recv failed with error: %d\n", WSAGetLastError());
	//关闭套接字
	closesocket(ConnectLessSocket);
	//释放资源
	WSACleanup();
	system("pause");
	return 0;
}

服务端:

#define _CRT_SECURE_NO_WARNINGS 1
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv)
{
	WSADATA wsaData;
	int iResult;
	SOCKET ServerSocket = INVALID_SOCKET;
	struct addrinfo *result = NULL;
	struct addrinfo hints;
	sockaddr_in clientaddr;
	int clientlen = sizeof(clientaddr);
	int iSendResult;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;
	//初始化WinSock
	iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != 0) {
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}
	ZeroMemory(&hints, sizeof(hints));
	//声明IPV4地址族,流式套接字,UDP协议
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_DGRAM;
	hints.ai_protocol = IPPROTO_UDP;
	hints.ai_flags = AI_PASSIVE;


	//解析服务器地址和端口号
	iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
	if (iResult != 0) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	//为无连接的服务器创建套接字
	ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
	if (ServerSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}

	//为监听套接字绑定本地地址和端口号 
	iResult = bind(ServerSocket, result->ai_addr, (int)result->ai_addrlen);
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ServerSocket);
		WSACleanup();
		return 1;
	}
	freeaddrinfo(result);

	printf("UDP server starting\n");
	ZeroMemory(&clientaddr, sizeof(clientaddr));

	//recvfrom函数直接在参数中指定接收数据的源地址
	iResult = recvfrom(ServerSocket, recvbuf, recvbuflen, 0, (SOCKADDR*)&clientaddr, &clientlen);
	if (iResult > 0) {
		//情况1:成功接收到数据
		printf("Bytes received: %d\n", iResult);
		//将缓冲区的内容回送给客户端
		//sendto函数也是同理,在参数中指定数据要发送到的目的地址
		iSendResult = sendto(ServerSocket, recvbuf, iResult, 0, (SOCKADDR*)&clientaddr, clientlen);
		if (iSendResult == SOCKET_ERROR){
			printf("send failed with error: %d\n", WSAGetLastError());
			closesocket(ServerSocket);
			WSACleanup();
			return 1;
		}
		printf("Bytes sent: %d\n", iSendResult);
	}
	else if (iResult == 0)
		//情况2:关闭连接
		printf("Connection closing...\n");
	else {
		//情况3:接收发生错误
		printf("recv failed with error: %d\n", WSAGetLastError());
		closesocket(ServerSocket);
		WSACleanup();
		return 1;
	}
	//关闭套接字
	closesocket(ServerSocket);
	//释放资源
	WSACleanup();
	system("pause");
	return 0;
}



  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L19002S

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值