IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> TCP/IP网络编程(3) -> 正文阅读

[网络协议]TCP/IP网络编程(3)

基于DUP的服务端与客户端

TCP/IP网络编程(2)中,介绍了TCP/IP的四层模型,传输层分为TCP和UDP两种方式,通过TCP套接字完成数据交换已经进行了介绍,下面介绍通过UDP套接字完成数据交换。

UDP套接字的特点

UDP的通信原理类似于寄送信件,在寄信之前,需要在信封上写好寄信人信息和收信人的地址信息,之后贴上邮票放进信箱即可。但是信件传输的特点,使我们无法确认收信方是否成功收到了信件,以及寄信过程总,信件是否发生了丢失或者损坏,即这是一种不可靠的通信方式。与之类似,UDP同样提供的是一种不不可靠的数据传输服务。

如果仅仅考虑数据传输的可靠性,TCP确实优于UDP,但是UPD在结构上比TCP更加的简单。UDP不会发送ACK确认消息,也不会给数据包分配序号,所以UDP的性能有时比TCP更高,且程序实现上也更加简单。此外,虽然UDP的可靠性比不上TCP,但是也不至于会频繁的发生数据丢失和数据损毁等情况。

TCP与UDP的区别:为了提供可靠的数据传输服务,TCP在不可靠的IP层进行了流控制,而UDP则缺少这种流控制机制。流控制是区分TCP与UDP的重要标志。TCP的速度无法超过UDP,但是在收发某些数据的时候有可能接近UDP,例如传输的数据量越大,TCP的传输速率就越接近UDP的传输速率。

UDP内部工作原理

如下图所示,UDP不会进行流控制,而IP层的作用就是让离开主机B的数据包准确的到达主机A.

将UDP数据包交给主机A的某一个套接字则是由UDP完成的。

?UDP最重要的作用就是根据端口号将传输到主机的数据包交付给最终的UDP套接字。

适合使用UDP的场景:网络传输特性可能会导致数据丢失。如果需要传输压缩包数据,则必须使用TCP进行传输,因为压缩文件只要丢失一小部分数据,就会影响数据的解压。但是在传输实时的视频或者是音频的时候,则丢失小部分数据也不会影响太大,只会引起画面短时间内的抖动,或者出现轻微的杂音,对于实时视频和音频而言,传输速度应该是优先考虑的问题,在这种应用场景下,TCP的数据流控制就显得有点多余,此时需要考虑使用UDP进行数据传输。

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

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

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

基于UDP的客户端与服务端程序设计

UDP服务端与客户端不像TCP那样需要在连接状态下进行数据交换。因此不必调用类似于listen和accep的功能的一些方法。UDP中只有创建套接字以及进行数据交换的过程。

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

套接字。UDP的套接字相当于寄信的邮筒,只要有一个邮筒,便可以向任何地址邮寄信件。同样地,只要有一个套接字,就可以向任意的主机传输数据。

?创建好TCP套接字,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方套接字的连接,即TCP套接字知道目标地址,但是UDP套接字不会保持这种连接状态,因此,每次传输数据都要添加目标地址信息,UDP套接字采用如下的方法实现数据的传输:

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

sock:用于传输数据的UDP套接字描述符

buff:保存待发送数据的缓冲区

nbytes: 传输数据的长度

flags:可选参数,若没有则可以设置为0

to:存有目标地址信息的sockaddr结构体变量地址

addrlen: 地址长度

与之相反,UDP套接字通过如下方法接收数据:

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:存有发送端地址信息的的sockaddr结构体变量地址

addrlen: 地址长度

用UDP实现服务端与客户端的计算器:

客户端calUdpClient实现:

// calUDPServer.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

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

// 报文格式定义
/*
+-------------------+-------------+
|       Type        |    Length   |
+-------------------+-------------+
|  identifier       |   ('u', 'k')|
+-------------------+-------------+
|  data length      |   2 Bytes   |
+-------------------+-------------+
|  operator count   |   2 Bytes   |
+-------------------+-------------+
|  operand_1        |   4 Bytes   |
+-------------------+-------------+
|  operand_2        |   4 Bytes   |
+-------------------+-------------+
|  operand_n...     |   4 Bytes   |
+-------------------+-------------+
|  operator(/+-*)   |   1 Bytes   |
+-------------------+-------------+
*/

// 数据包相关的宏定义
#define BUFF_SIZE             100          // 数据包缓冲区大小
#define MESSAGE_HEADER_SIZE   4            // 消息头占四个字节
#define MESSAGE_HEADER_CHAR1  'U'          
#define MESSAGE_HEADER_CHAR2  'K'
#define RESULT_OVERFLOW       -999999      // 计算结果溢出
#define OPERAND_SIZE          4            // 运算数大小为4字节
#define RESULT_LEN            4            // 运算结果大小为4字节

#define SERVER_ADDR           "127.0.0.1"  // 服务端地址
#define SERVER_PORT           19800        // 服务端通信端口

void error_handler(char* msg)
{
	printf("%s\n", msg);
	system("pause");
	exit(1);                 // 退出程序
}

typedef unsigned short  ushort;
typedef INT16           int16;
typedef INT32           int32;


int main()
{
	WSADATA wsadata;
	SOCKET clientSock;          // 客户端socket

	sockaddr_in servAddr;       // 服务端地址,用于向服务器发送数据

	sockaddr_in fromAddr;       // 数据来源的地址信息

	int addrLen = sizeof(servAddr);

	char buffer[BUFF_SIZE];
	memset(buffer, 0, BUFF_SIZE);

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handler("Failed to init the win socket lib!");
	}

	clientSock = socket(PF_INET, SOCK_DGRAM, 0);
	if (clientSock == INVALID_SOCKET)
	{
		error_handler("Failed to create the socket!");
	}

	// 初始化服务端地址
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
	servAddr.sin_port = htons(SERVER_PORT);

	while (true)
	{
		ushort operandCount = 0;
		printf("Please input operand count: ");
		scanf("%d", &operandCount);

		// 填充消息
		buffer[0] = MESSAGE_HEADER_CHAR1;
		buffer[1] = MESSAGE_HEADER_CHAR2;

		// 数据长度
		buffer[2] = 0;
		buffer[3] = 0;

		// 运算数的个数 2字节
		buffer[4] = (char)operandCount & 0xff;
		buffer[5] = (char)((operandCount >> 8) & 0xff);

		// 填充操作数
		for (int i=0; i<operandCount; ++i)
		{
			printf("Please Input operand %d: ", i + 1);
			scanf("%d", (int32*)&buffer[MESSAGE_HEADER_SIZE + 2 + i*OPERAND_SIZE]);
		}

		// 填充运算符
		printf("Please input operator: ");
		scanf(" %c", &buffer[MESSAGE_HEADER_SIZE + 2 + operandCount*OPERAND_SIZE]);

		// 最后填充数据长度
		int dataLen = MESSAGE_HEADER_SIZE + 2 + operandCount * OPERAND_SIZE + 1;

		buffer[2] = dataLen & 0x00ff;
		buffer[3] = dataLen & 0xff00;

		// 发送数据包
		int sendLen = sendto(clientSock, buffer, BUFF_SIZE, 0, (sockaddr*)&servAddr, addrLen);

		// 接收数据包
		int result;
		int recvLen = recvfrom(clientSock, (char*)&result, RESULT_LEN, 0, (sockaddr*)&fromAddr, &addrLen);

		if (recvLen != RESULT_LEN)
		{
			printf("Received invalid result %d .\n", result);
			memset(buffer, 0, BUFF_SIZE);
			continue;
		}

		if (result == RESULT_OVERFLOW)
		{
			printf("The result is over flow.\n");
		}
		else
		{
			printf("The result is: %d\n", result);
		}

		memset(buffer, 0, BUFF_SIZE);
	}

	closesocket(clientSock);
	WSACleanup();

	return 0;
}


服务端calUdpServer:

// calUDPClient.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>

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

// 报文格式定义
/*
+-------------------+-------------+
|       Type        |    Length   |
+-------------------+-------------+
|  identifier       |   ('u', 'k')|
+-------------------+-------------+
|  data length      |   2 Bytes   |
+-------------------+-------------+
|  operator count   |   2 Bytes   |
+-------------------+-------------+
|  operand_1        |   4 Bytes   |
+-------------------+-------------+
|  operand_2        |   4 Bytes   |
+-------------------+-------------+
|  operand_n...     |   4 Bytes   |
+-------------------+-------------+
|  operator(/+-*)   |   1 Bytes   |
+-------------------+-------------+
*/

void error_handler(char* msg)
{
	printf("%s\n", msg);
	system("pause");
	exit(1);                 // 退出程序
}

// 数据包相关的宏定义
#define BUFF_SIZE             100          // 数据包缓冲区大小
#define SERVER_PORT           19800        // 服务端通信端口
#define MESSAGE_HEADER_SIZE   4            // 消息头占四个字节
#define MESSAGE_HEADER_CHAR1  'U'          
#define MESSAGE_HEADER_CHAR2  'K'
#define RESULT_OVERFLOW       -999999      // 计算结果溢出
#define OPERAND_SIZE          4            // 运算数大小为4字节
#define RESULT_LEN            4            // 运算结果大小为4字节



typedef unsigned short  ushort;
typedef INT16           int16;
typedef INT32           int32;

int main()
{
	WSADATA wsadata;
	SOCKET servSock;            // 服务端套接字
	sockaddr_in servAddr;       // 服务端地址
	sockaddr_in clientAddr;     // 客户端的地址信息

	int addrlen = sizeof(clientAddr);

	int recvLen = 0;                // 单次接受数据长度
	int recvTotalLen = 0;           // 接收数据的中长度

	char buffer[BUFF_SIZE];       // 缓冲区
	memset(buffer, 0, BUFF_SIZE);

	int result = 0;

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
	{
		error_handler("Failed to init win socket lib");
	}

	servSock = socket(PF_INET, SOCK_DGRAM, 0);          // 初始化UDP套接字
	if (servSock == INVALID_SOCKET)
	{
		error_handler("Failed to create the socket!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;                      // 初始化地址族
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);       // 初始化地址
	servAddr.sin_port = htons(SERVER_PORT);             // 初始化端口

	if (bind(servSock, (sockaddr*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
	{
		error_handler("Failed to bind the server socket and address.\n");
	}

	while (true)
	{
		printf("Waiting for receiving message...\n");
		recvLen = recvfrom(servSock, buffer, BUFF_SIZE, 0, (sockaddr*)&clientAddr, &addrlen);            // 注意这里接受长度写为BUFF_SIZE-1的话会导致最终接收的长度为零

		if (recvLen <= 0)
		{
			printf("Not received data from client.\n");
			continue;
		}

		recvTotalLen += recvLen;

		if (recvTotalLen >= MESSAGE_HEADER_SIZE)    // 首先判断报文头完整
		{
			if (buffer[0] != MESSAGE_HEADER_CHAR1 || buffer[1] != MESSAGE_HEADER_CHAR2)
			{
				// 重新接收消息
				printf("The message header is invalid.\n");
				memset(buffer, 0, BUFF_SIZE);
				recvTotalLen = 0;
				continue;
			}

			int dataLength = buffer[2] | buffer[3] << 8;

			if (dataLength == 0)
			{

			}

			while (recvTotalLen < dataLength + MESSAGE_HEADER_SIZE)
			{
				// 数据未接受完全
				recvLen = recvfrom(servSock, &buffer[recvTotalLen], BUFF_SIZE - 1 - recvTotalLen, 0, (sockaddr*)&clientAddr, &addrlen);
				recvTotalLen += recvLen;
			}

			// 接收数据成功,开始解析数据
			// 函数innet_aton()和函数inet_ntoa(in_addr inaddr)功能相反  in_addr 是 sockarr_in.sin_addr类型
			printf("Sccessfully received %d bytes data from adress: %s.\n", recvTotalLen, inet_ntoa(clientAddr.sin_addr));

			int operandCount = buffer[MESSAGE_HEADER_SIZE] | buffer[MESSAGE_HEADER_SIZE + 1];

			char calOperator = buffer[MESSAGE_HEADER_SIZE + 2 + operandCount*OPERAND_SIZE];

			if (calOperator != '+' &&
				calOperator != '-' &&
				calOperator != '*' &&
				calOperator != '/')
			{
				// 运算符错误
				printf("The opertor %c is invalid!\n", calOperator);
				memset(buffer, 0, BUFF_SIZE);
				recvTotalLen = 0;
				continue;
			}

			for (int i = 0; i < operandCount; ++i)
			{
				int32 operand = buffer[MESSAGE_HEADER_SIZE + 2 + i*OPERAND_SIZE];

				if (i == 0)
				{
					result = operand;
					continue;
				}

				if (calOperator == '+')
				{
					result += operand;
				}
				else if (calOperator == '-')
				{
					result -= operand;
				}
				else if (calOperator == '*')
				{
					result *= operand;
				}
				else
				{
					if (operand == 0)
					{
						result = RESULT_OVERFLOW;
						break;
					}

					result /= operand;
				}

			}

			sendto(servSock, (char*)&result, RESULT_LEN, 0, (sockaddr*)&clientAddr, addrlen);
			printf("The calculator result %d has been send to %s\n", result, inet_ntoa(clientAddr.sin_addr));
			// 结束解析以及发送计算结果后进行的处理
			// 重新接收消息
			memset(buffer, 0, BUFF_SIZE);
			recvTotalLen = 0;
			result = 0;
		}
		else
		{
			// 重新接收消息
			memset(buffer, 0, BUFF_SIZE);
			recvTotalLen = 0;
			continue;
		}
	}

	closesocket(servSock);
	WSACleanup();
	return 0;
}

运行结果如下所示:

客户端输入计算数据:

?服务端返回计算结果:

?在上述的例子中,没有为UDP客户端套接字分配地址以及端口的地方,在TCP中,在connect的时候,会自动为客户端套接地分配地址以及端口,而在UDP中并没有相应的操作。实际上,在调用sendto函数完成数据传输前,应该完成对套接字的地址分配工作,因此在客户端调用了bind函数。在TCP中页调用了bind函数,实际上bind函数是不区分UDP与TCP的。此外,如果在调用sendto函数时,发现尚未分配地址以及端口号,则在首次调用sendto函数的时候给套接字分配地址以及端口,而且此时分配的地址会一直保持到程序结束为止,因此也可以用来与其他的UDP套接字进行数据交换,所以客户端在调用sendto的时候,为套接字自动分配了IP和端口号。因此UDP客户端中通常无需额外的为套接字分配地址以及端口号。

UDP的数据传输特性

TCP数据传输不存在边界,意味着数据传输过程中调用IO函数的次数不具有任何意义。而UDP是具有数据边界的协议,在数据传输过程中调用IO函数的次数非常重要,发送函数的调用次数和接收函数的调用次数必须保持一致,这样才能保证数据已经全部发送。

已连接(connected)的UDP套接字和未连接(unconnected)的UDP套接字

TCP套接字需要注册待传输数据的IP地址以及端口号,而在UDP中无需注册,通过sendto函数传输数据的过程,大致可以分为三个阶段:

  1. 向UDP套接字注册目标IP以及端口号
  2. 进行数据传输
  3. 删除UDP套接字中注册的目标地址信息

每次调用sendto()函数传输数据,会重复进行上述流程,每次都会变更目标地址以及端口号,因此可以利用同一UDP套接字多次向不同地址传输数据。这种未注册目标地址信息的套接字称为未连接的套接字,而注册了目标地址和端口信息的套接字称为已连接的套接字。UDP套接字默认为未注册的套接字。但是若存在这样的情形:UDP套接字需要向统一目标地址传输10次数据,则需要调用10次sendto()函数,因此UDP套接字需要反复执行10次注册流程,导致效率降低。在需要与一台主机进行长时间的通信时,将UDP套接字编程已连接的套接字会提高效率。在上面的三个步骤中,步骤1和2的时间占所有步骤时间的三分之一,省去反复注册和删除的流程可大大提高通信的效率。

以上面的代码为例:
?

....... 省略......
// 初始化服务端地址
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
	servAddr.sin_port = htons(SERVER_PORT);

	// 注册UDPsocket,编程connected类型的socket
	connect(clientSock, (sockaddr*)&servAddr, sizeof(servAddr));

	while (true)
    {
       ....... 省略......
    }

-----------------------------------------------分割线-----------------------------------------------

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-04-07 23:04:59  更:2022-04-07 23:06:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 4:22:38-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码