基于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)
{
....... 省略......
}
-----------------------------------------------分割线-----------------------------------------------
|