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函数传输数据的过程,大致可以分为三个阶段:
- 向UDP套接字注册目标IP以及端口号
- 进行数据传输
- 删除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) { ....... 省略...... }
-----------------------------------------------分割线-----------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)