基于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;          // 客户端socketsockaddr_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类型的socketconnect(clientSock, (sockaddr*)&servAddr, sizeof(servAddr));while (true){....... 省略......}

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

TCP/IP网络编程(3)相关推荐

  1. 高等学校计算机科学与技术教材:tcp/ip网络编程技术基础,TCP/IP网络编程技术基础...

    TCP/IP网络编程技术基础 语音 编辑 锁定 讨论 上传视频 <TCP/IP网络编程技术基础>是2012年北京交通大学出版社出版的图书,作者是王雷. 书    名 TCP/IP网络编程技 ...

  2. TCP/IP网络编程之基于TCP的服务端/客户端(二)

    回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...

  3. TCP/IP网络编程(1)

    1. 套接字 套接字是由操作系统提供的网络数据通信软件设备,即使对网络数据传输原理不了解,也能够使用套接字完成网络数据传输.为了与远程计算机进行数据传输,需要连接到英特网,套接字就是进行网络连接的工具 ...

  4. 《TCP/IP网络编程》第20章

    <TCP/IP网络编程>第20章 同步方法分类及CRITICAL_SECTION同步 用户模式(User mode)和内核模式(Kernal mode) 用户模式同步 内核模式同步 基于C ...

  5. TCP/IP网络编程之多进程服务端(一)

    TCP/IP网络编程之多进程服务端(一) 进程概念及应用 我们知道,监听套接字会有一个等待队列,里面存放着不同客户端的连接请求,如果有一百个客户端,每个客户端的请求处理是0.5s,第一个客户端当然不会 ...

  6. TCP/IP网络编程之基于TCP的服务端/客户端(一)

    TCP/IP网络编程之基于TCP的服务端/客户端(一) 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于 ...

  7. TCP/IP网络编程之多进程服务端(二)

    TCP/IP网络编程之多进程服务端(二) 信号处理 本章接上一章TCP/IP网络编程之多进程服务端(一),在上一章中,我们介绍了进程的创建和销毁,以及如何销毁僵尸进程.前面我们讲过,waitpid是非 ...

  8. 网络编程+go+java,Go语言中的TCP/IP网络编程

    Go语言TCP/IP网络编程 乍一看,通过TCP/IP层连接两个进程会感觉可怕, 但是在Go语言中可能比你想象的要简单的多. TCP/IP层发送数据的应用场景 当然很多情况下,不是大多数情况下,使用更 ...

  9. tcp/ip网络编程--accept()函数返回的套接字

    tcp/ip网络编程–accept()函数返回的套接字 套接字:1)套接字是对网络中不同主机的应用进程之间进行双向通信的端点的抽象:一个套接字就是网络进程通信的一端.[1] 2)套接字是用来与另一个进 ...

最新文章

  1. platform_driver_register适配的两种方式及probe是否启动与硬件关系
  2. windows环境下32位汇编语言程序设计 90盘_Python 0基础详细教程 环境安装01
  3. 全国公共英语五级(PETS-5) 写作作文
  4. 数据结构与算法 总结
  5. C#实例:datagridview单元格合并
  6. ubuntu nginx配置负载均衡篇(一)
  7. C# 将内存中的datatable数据导出为Excel(方法二,创建Excel对象导出)
  8. TensorFlow tf.keras.layers.Permute
  9. 2021-08-27 向量究竟是什么?线性代数的本质,第1章
  10. spring4.1.8扩展实战之七:控制bean(BeanPostProcessor接口)
  11. linux内存管理方式,简要概括Linux内存管理的方式
  12. springboot配置C3P0数据库连接池
  13. 单片机炫彩灯实训报告_单片机跑马灯实验报告
  14. hive sql系列(二)——统计每个人每个月访问量和累计月访问量
  15. 图像处理算法之模糊检测
  16. 2021年美容师(初级)考试内容及美容师(初级)模拟考试题
  17. 【C语言小题】分数求和
  18. Java知识点串讲之面向对象简述
  19. 新网站如何快速被收录?网站收录如何查询
  20. LeetCode刷题第8天字符串系列之《378字符串中的第一个唯一字符》

热门文章

  1. k8s核心技术-Controller(statefulSet)_部署有状态应用---K8S_Google工作笔记0033
  2. RabbitMq学习笔记002---RabbitMq在SpringBoot中的应用_配置_使用_并且设置优先级
  3. inception V4 与resnet
  4. java n*n矩阵求值及求逆矩阵
  5. 神奇的go语言(开始篇)
  6. 随想录(从开源的宝库中学习)
  7. 随想录(linux下的pv操作)
  8. C语言除法浮点型和整形,浅谈C语言整型与浮点型转换
  9. 关闭虚拟机linux的防火墙,Linux虚拟机SSH服务、防火墙开启关闭
  10. Linux里sra文件是什么,prefetch命令下载SRA文件