UDP(User Datagram Protocol,用户数据报协议)是无连接的,因此在两个进程通信前没有握手过程。UDP协议提供一种不可靠数据传送服务,也就是说,当进程将一个报文发送进UDP套接字时,UDP协议并不保证该报文将到达接收进程。不仅如此,到达接收进程的报文也可能是乱序到达的。从一个端系统向另一个端系统发送独立的数据分组,不对交付提供任何保证。运行在不同机器上的进程彼此通过向套接字(socket)发送报文(message)来进行通信。UDP是一种不提供不必要服务的轻量级运输协议,它仅提供最小服务。
UDP没有包括拥塞控制机制,所以UDP的发送端可以用它选定的任何速率向其下层(网络层)注入数据。(然而,值得注意的是实际端到端吞吐量可能小于该速率,这可能是因为中间链路的带宽受限或因为拥塞而造成的。)
使用UDP套接字的两个通信进程之间的交互:在发送进程能够将数据分组推出套接字之门之前,当使用UDP时,必须先将目的地址附在该分组之上。在该分组传过发送方的套接字之后,因特网将使用该目的地址通过因特网为该分组选路到接收进程的套接字。当分组到达接收套接字时,接收进程将通过该套接字取回分组,然后检查分组的内容并采取适当的动作。附在分组上的目的地址包含了什么?目的主机的IP地址是目的地址的一部分。通过在分组中包括目的地的IP地址,因特网中的路由器将能够通过因特网分组选路到目的主机。但是因为一台主机可能运行许多网络应用进程,每个进程具有一个或多个套接字,所以在目的主机指定特定的套接字也是必要的。当生成一个套接字时,就为它分配一个称为端口号(port number)的标识符。因此,分组的目的地址也包括该套接字的端口号。总的来说,发送进程为分组附上目的地址,该目的地址是由目的主机的IP地址和目的地套接字的端口号组成的。此外,发送方的源地址也是由源主机的IP地址和源套接字的端口号组成,该源地址也要附在分组之上。然而,将源地址附在分组之上通常并不是由UDP应用程序代码所为,而是由底层操作系统自动完成的。
使用UDP的客户----服务器应用程序如下图所示:在客户发送其报文之前,服务器必须作为一个进程正在运行。TCP在开始数据传输之前要经过三次握手,UDP却不需要任何准备即可进行数据传输。因此UDP不会引入建立连接的时延。
注:以上内容来自《计算机网络自顶向下方法(原书第7版)》
以下是测试代码:参考:https://www.binarytides.com/udp-socket-programming-in-winsock/
服务器端:
int test_socket_udp_server()
{
#ifdef _MSC_VER
// Initialise winsock
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "Failed. Error Code : %d", WSAGetLastError());
return -1;
}
// Create a socket
SOCKET s;
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET) {
fprintf(stderr, "Could not create socket : %d", WSAGetLastError());
return -1;
}
// Prepare the sockaddr_in structure
struct sockaddr_in server, si_other;
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(SERVER_PORT);
// Bind
if (bind(s, (struct sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) {
fprintf(stderr, "Bind failed with error code : %d", WSAGetLastError());
return -1;
}
// keep listening for data
int slen = sizeof(si_other);
int recv_len;
char buf[BUFFER_MAX_LEN];
while (1) {
fprintf(stdout, "Waiting for data...");
fflush(stdout);
// clear the buffer by filling null, it might have previously received data
memset(buf, '\0', BUFFER_MAX_LEN);
//try to receive some data, this is a blocking call
if ((recv_len = recvfrom(s, buf, BUFFER_MAX_LEN, 0, (struct sockaddr*)&si_other, &slen)) == SOCKET_ERROR) {
fprintf(stderr, "recvfrom failed with error code : %d", WSAGetLastError());
return -1;
}
// print details of the client/peer and the data received
fprintf(stdout, "Received packet from %s:%d\n", inet_ntoa(si_other.sin_addr), ntohs(si_other.sin_port));
fprintf(stdout, "Data: %s\n", buf);
int i = 0;
while (buf[i]) {
buf[i] = toupper(buf[i]);
++i;
}
// now reply the client with the same data
if (sendto(s, buf, recv_len, 0, (struct sockaddr*)&si_other, slen) == SOCKET_ERROR) {
fprintf(stderr, "sendto failed with error code : %d", WSAGetLastError());
return -1;
}
}
closesocket(s);
WSACleanup();
return 0;
#else
fprintf(stderr, "the Linux platform is not yet implemented\n");
return -1;
#endif
}
客户端:
int test_socket_udp_client()
{
#ifdef _MSC_VER
// Initialise winsock
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "Failed. Error Code : %d", WSAGetLastError());
return -1;
}
// create socket
SOCKET s;
if ((s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == SOCKET_ERROR) {
fprintf(stderr, "socket failed with error code : %d", WSAGetLastError());
return -1;
}
// setup address structure
struct sockaddr_in si_other;
int slen = sizeof(si_other);
memset((char*)&si_other, 0, sizeof(si_other));
si_other.sin_family = AF_INET;
si_other.sin_port = htons(SERVER_PORT);
si_other.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);
char buf[BUFFER_MAX_LEN];
char message[BUFFER_MAX_LEN];
// start communication
while (1) {
fprintf(stdout, "Enter message : ");
gets_s(message);
// send the message
if (sendto(s, message, strlen(message), 0, (struct sockaddr*)&si_other, slen) == SOCKET_ERROR) {
fprintf(stderr, "sendto failed with error code : %d", WSAGetLastError());
return -1;
}
// receive a reply and print it
// clear the buffer by filling null, it might have previously received data
memset(buf, '\0', BUFFER_MAX_LEN);
// try to receive some data, this is a blocking call
if (recvfrom(s, buf, BUFFER_MAX_LEN, 0, (struct sockaddr*)&si_other, &slen) == SOCKET_ERROR) {
fprintf(stderr, "recvfrom failed with error code : %d", WSAGetLastError());
return -1;
}
puts(buf);
}
closesocket(s);
WSACleanup();
return 0;
#else
fprintf(stderr, "the Linux platform is not yet implemented\n");
return -1;
#endif
}
服务器端IP地址和端口号的设置如下:
#define SERVER_IP "127.0.0.1" // ip address of udp server
#define BUFFER_MAX_LEN 512 // Max length of buffer
#define SERVER_PORT 12345 // The port on which to listen for incoming data
recvfrom函数声明如下:用于接收数据报(datagram)
int recvfrom(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags,
[out] sockaddr *from,
[in, out, optional] int *fromlen
);
参数介绍:
(1).s:绑定socket的描述符(descriptor)。
(2).buf:传入数据的buffer.
(3).len:buf参数指向的buffer的长度(以字节为单位).
(4).flags:用于修改函数调用的行为。
(5).from:指向sockaddr结构中的buffer的指针。
(6).fromlen:from参数指向的buffer的大小(以字节为单位).
如果没有发生错误,recvfrom函数返回接收的字节数。如果连接已正常关闭,则返回值为0.否则将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError函数获取错误码。
recvfrom函数读取已连接和未连接socket上的传入数据(incoming data),并捕获发送数据的地址。此函数通常用于无连接socket.必须知道socket的本地地址(local address)。对于server应用程序,这通常通过bind函数显式地完成。client应用程序不建议使用显式绑定。对于使用此函数的client应用程序,socket可以通过sendto, WSASendTo或WSAJoinLeaf隐式绑定到本地地址。
对于UDP,如果接收到的数据包(packet)不包含数据(空),则recvfrom函数将返回0.
如果from参数非0,并且socket不是面向连接的(例如类型为SOCK_DGRAM),则发送数据的对等方的网络地址(network address of the peer)将复制到相应的sockaddr结构中。fromlen所指向的值被初始化为该结构的大小,并在返回时被修改,以指示存储在sockaddr结构中的地址的实际大小。
如果socket上没有可用的传入数据,则recvfrom函数将根据为WSARecv定义的阻塞规则阻塞并等待数据到达,除非socket是非阻塞的,否则不设置MSG_PARTIAL标志。
sendto函数声明如下:用于发送数据
int sendto(
[in] SOCKET s,
[in] const char *buf,
[in] int len,
[in] int flags,
[in] const sockaddr *to,
[in] int tolen
);
参数介绍:
(1).s:socket的描述符。
(2).buf:要传输数据的buffer的指针。
(3).len:buf参数指向的数据的长度(以字节为单位)。
(4).flags:用于指定调用的方式.
(5).to:指向scokaddr结构的指针,该结构包含目标socket的地址。
(6).tolen:to参数指向的地址的大小(以字节为单位)。
如果没有发生错误,sendto将返回发送的总字节数,这可能小于len所指示的值。否则,将返回SOCKET_ERROR,并且可以通过调用WSAGetLastError函数获取错误码。
to参数可以是socket地址族中的任何有效地址,包括广播地址和组播地址(broadcast or any multicast address)。要发送到广播地址,应用程序必须使用启用了SO_BROADCAST的setsockopt。否则,sendto将失败,错误码为WSAEACCES。
如果打开socket,进行setsockopt调用,然后进行sendto调用,则Windows Sockets将执行隐式绑定函数调用。
如果socket未绑定(unbound),则系统将为本地关联分配唯一值,然后将socket标记为绑定。
sendto的成功完成并不表示数据已成功传递。
sendto函数通常用于无连接socket,将数据报发送到由to参数标识的特定对等方socket。即使无连接socket以前已连接到特定地址,to参数也仅覆盖该特定数据报的目标地址。
以上测试代码实现的功能是,客户端发送字符串,服务器端接收后将其转换为大写字母后再发送,执行结果如下图所示:先运行服务器端程序,左半图,release模式;右半图为debug下的客户端程序
GitHub: https://github.com/fengbingchun/OpenSSL_Test