寫在前面
UDP(User Datagram Protocol)稱為用戶數(shù)據(jù)報協(xié)議,是一種無連接的傳輸協(xié)議。
UDP的主要應(yīng)用在即使丟失部分?jǐn)?shù)據(jù),也不影響整體效果的場景。例實時傳輸視頻或音頻時,即使丟失部分?jǐn)?shù)據(jù),也不會影響整體效果,只是會有輕微的畫面抖動或雜音。
UDP中的服務(wù)器/客戶端沒有連接
UDP服務(wù)器/客戶端不像TCP那樣,交換數(shù)據(jù)前需進行connect和accept進行連接。UDP中只有創(chuàng)建套接字和數(shù)據(jù)交互的過程。
UDP服務(wù)器和客戶端均只需要一個套接字
在TCP服務(wù)器/客戶端程序中,套接字是一一對應(yīng)的關(guān)系。服務(wù)器若要向10個客戶端提供服務(wù),除了監(jiān)聽套接字外,還需要10個對應(yīng)客戶端的服務(wù)器套接字。
而在UDP中,不管是服務(wù)器還是客戶端均只有一個套接字。在服務(wù)器端,可以通過服務(wù)器端的這個套接字向多個不同的客戶端提供服務(wù)。同理,在客戶端,也可以通過客戶端的這個套接字向不同服務(wù)器請求服務(wù)。
基于UDP的IO函數(shù)
創(chuàng)建好TCP套接字后,需要事先通過bind函數(shù)綁定IP和端口,并維持和對方的連接。
而UDP沒有綁定IP和端口的步驟,因此不會保持連接。那么傳輸數(shù)據(jù)時就需要在IO函數(shù)中指定要目的地地址。
#include <winsock2.h>
int WSAAPI sendto(SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen);
//s: 標(biāo)識 (可能連接的) 套接字的描述符
//buf: 指向包含要傳輸數(shù)據(jù)的緩沖區(qū)的指針
//len: buf 參數(shù)指向的數(shù)據(jù)長度(以字節(jié)為單位)
//flags: 一組標(biāo)志,用于指定調(diào)用的進行方式
//to: 指向包含目標(biāo)套接字地址的 sockaddr 結(jié)構(gòu)的可選指針
//tolen: 參數(shù)指向的地址的大?。ㄒ宰止?jié)為單位)
//返回值:如果未發(fā)生錯誤, sendto 將返回發(fā)送的字節(jié)總數(shù),這可能小于 len 指示的數(shù)字。 否則,將返回SOCKET_ERROR值
int WSAAPI recvfrom( SOCKET s, char *buf, int len, int flags, sockaddr* from, int* fromlen);
//s: 標(biāo)識綁定套接字的描述符
//buf: 傳入數(shù)據(jù)的緩沖區(qū)
//len: buf 參數(shù)指向的緩沖區(qū)的長度(以字節(jié)為單位)
//flags: 一組選項,用于修改函數(shù)調(diào)用的行為,超出為關(guān)聯(lián)套接字指定的選項
//from: 指向 sockaddr 結(jié)構(gòu)中緩沖區(qū)的可選指針,將在返回時保存源地址, 注意這是一個輸出參數(shù)
//fromlen: 指向 參數(shù) 指向的緩沖區(qū)大?。ㄒ宰止?jié)為單位)的可選指針
下面將給出基于UDP的服務(wù)器/客戶端代碼示例。
服務(wù)器
// UDP_Server.cpp : 定義控制臺應(yīng)用程序的入口點。
//
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
//服務(wù)器端UDP套接字,第三個參數(shù)也可傳0
SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == srvSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
//服務(wù)器端地址信息
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
int srvAddrLen = sizeof(srvAddr);
//因為該示例先使用recvfrom從客戶端接收數(shù)據(jù),因此這里需先調(diào)用bind函數(shù)綁定服務(wù)器端UDP套接字的地址信息。否則接收到的數(shù)據(jù)不知道要給哪個應(yīng)用程序
//可嘗試屏蔽這部分,查看終端是否會打印客戶端發(fā)來的數(shù)據(jù)
if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("bind Error!\n");
closesocket(srvSock);
WSACleanup();
return -1;
}
//這里事先創(chuàng)建并初始化客戶端的地址信息變量。
//之后recvfrom時會添加該變量值
SOCKADDR_IN cltAddr;
memset(&cltAddr, 0, sizeof(cltAddr));
int nCltAddrLen = sizeof(cltAddr);
char Msg[BUF_SIZE];
int recvLen = 0;
while (true)
{
printf("Wait Msg From Client...\n");
//無連接的UDP套接字,sendto和recvfrom必須一一對應(yīng),也沒有數(shù)據(jù)邊界
//無連接的UDP套接字不會保持連接狀態(tài),因此每次傳輸數(shù)據(jù)都需要添加目標(biāo)地址信息。
//第五、六個參數(shù)用來填充保存客戶端的地址信息
//這里使用一個服務(wù)器端UDP套接字srvSock,從多個客戶端接收數(shù)據(jù)
recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen);
if (recvLen == -1)
{
//UDP屬于無連接協(xié)議,因此沒有斷開連接的說法,這里不會進來
printf("Client Disconnected.");
break;
}
Msg[recvLen] = 0;
printf("Receive Msg from Client: %s\n", Msg);
//第五、六個參數(shù)為上面保存的客戶端的地址信息
//這里使用一個服務(wù)器端UDP套接字srvSock,回復(fù)多個客戶端,通過接收數(shù)據(jù)時填充的地址信息標(biāo)識多個客戶端
sendto(srvSock, Msg, recvLen, 0, (sockaddr*)&cltAddr, nCltAddrLen);
}
closesocket(srvSock);
WSACleanup();
return 0;
}
客戶端
// UDP_Client.cpp : 定義控制臺應(yīng)用程序的入口點。
//
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
//客戶端UDP套接字
SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == cltSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
//服務(wù)器端地址信息
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
srvAddr.sin_port = htons(_ttoi(argv[2]));
int nSrvAddrLen = sizeof(srvAddr);
char Msg[BUF_SIZE];
int recvLen = 0;
while (true)
{
fputs("Input Msg(q to quit): ", stdout);
fgets(Msg, sizeof(Msg), stdin);
if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n"))
{
printf("Disconnected...");
break;
}
//這里向地址信息為srvAddr傳輸數(shù)據(jù)
//這里客戶端UDP套接字cltSock沒有事先綁定IP和端口,因此每次調(diào)用sendto時都會自動分配IP和端口
sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, sizeof(srvAddr));
recvLen = recvfrom(cltSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &nSrvAddrLen);
Msg[recvLen] = 0;
printf("Msg From Server: %s\n", Msg);
}
closesocket(srvSock);
WSACleanup();
return 0;
}
UDP客戶端套接字地址分配
還記得此前的TCP客戶端代碼,通過connect函數(shù)連接服務(wù)器,并自動完成客戶端套接字的IP和端口分配。
而在UDP程序中,調(diào)用sendto函數(shù)傳輸數(shù)據(jù)前應(yīng)該完成對套接字的地址分配工作。
在UDP中能否通過bind函數(shù)為套接字綁定IP和端口呢?答案是可以的,因為bind不區(qū)分UDP和TCP,因此在UDP程序中也可以使用。
另外,調(diào)用sendto函數(shù)時發(fā)送尚未分配地址信息,則在首次調(diào)用sendto函數(shù)時給相應(yīng)套接字自動分配IP和端口,而且此時分配的IP和端口會一直保留到程序結(jié)束為止。
綜上,在UDP中,調(diào)用sendto函數(shù)時會自動分配地址信息。
存在數(shù)據(jù)邊界的UDP套接字
UDP是具有數(shù)據(jù)邊界的協(xié)議,這意味著輸入函數(shù)的調(diào)用次數(shù)必須嚴(yán)格對應(yīng)輸出函數(shù)的調(diào)用次數(shù),這樣才能接收到完整的數(shù)據(jù)。
這里在客戶端調(diào)用三次sendto函數(shù),即發(fā)送三次數(shù)據(jù)到服務(wù)器,在服務(wù)器端只調(diào)用一次recvfrom函數(shù)試圖接收所有數(shù)據(jù),這是行不通的。
服務(wù)器端代碼如下:
//UDP_Server.cpp
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == srvSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
int srvAddrLen = sizeof(srvAddr);
if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("bind Error!\n");
closesocket(srvSock);
WSACleanup();
return -1;
}
char Msg[BUF_SIZE];
int recvLen = 0;
SOCKADDR_IN cltAddr;
memset(&cltAddr, 0, sizeof(cltAddr));
int nCltAddrLen = sizeof(cltAddr);
recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen);
Msg[recvLen] = 0;
printf("recvfrom client msg: %s\n", Msg);
closesocket(srvSock);
WSACleanup();
getchar();
return 0;
}
客戶端代碼如下:
//UDP_Client.cpp
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == cltSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
srvAddr.sin_port = htons(_ttoi(argv[2]));
int nSrvAddrLen = sizeof(srvAddr);
char Msg[BUF_SIZE] = "0123456789";
int recvLen = 0;
//客戶端發(fā)送三次數(shù)據(jù),服務(wù)器端調(diào)用一次recvfrom試圖接收三次數(shù)據(jù)。是行不通的
int nSendLend = 0;
//因為沒有事先為cltSock分配地址信息,因此這里每次調(diào)用都會自動分配IP和端口
nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
closesocket(cltSock);
WSACleanup();
getchar();
return 0;
}
服務(wù)器端也必須調(diào)用相應(yīng)次數(shù)的recvfrom才能接收到客戶端發(fā)來的完整數(shù)據(jù):
//調(diào)整UDP_Server.cpp中接收數(shù)據(jù)部分處理
//上文同上,故省略
for (int i = 0; i < 3; i++)
{
recvLen = 0;
recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&cltAddr, &nCltAddrLen);
Msg[recvLen] = 0;
printf("recvfrom client msg: %s\n", Msg);
}
//下文同上,故省略
已連接(connected)和未連接(unconnected)的UDP套接字
TCP套接字中需注冊待傳輸數(shù)據(jù)的目標(biāo)IP和端口號,而UDP中無需事先注冊。因此,通過sendto函數(shù)傳輸數(shù)據(jù)的過程大致可分為以下3個階段:
①向UDP套接字注冊目標(biāo)IP和端口號
②傳輸數(shù)據(jù)
③刪除UDP套接字中注冊的模板地址信息
每次調(diào)用sendto函數(shù)時重復(fù)上述過程。每次都變更目標(biāo)地址,因此可以重復(fù)利用同一UDP套接字向不同目標(biāo)傳輸數(shù)據(jù)。
這種事先注冊目標(biāo)地址信息,在sendto時才注冊的套接字稱為未連接套接字。反之事先注冊了目標(biāo)地址的套接字稱為連接套接字。顯然UDP套接字默認(rèn)屬于未連接套接字。
但UDP套接字在只需向一個目標(biāo)地址傳輸數(shù)據(jù)時就顯得不太合理。
例:IP為169.21.32.110的主機9190號端口共準(zhǔn)備了3個數(shù)據(jù),因此需要調(diào)用三次sendto函數(shù)進行傳輸。
此時需要重復(fù)上述三階段,上述三個階段中,第①、③個階段占整個通信過程近1/3的時間,縮短這部分時間將大大提高整體效率。
因此,要與同一主機進行長時間通信時,將UDP套接字編程連接套接字會提供效率。
如何將UDP套接字變成連接套接字?
對UDP套接字調(diào)用connect函數(shù)即可。
修改上面服務(wù)器/客戶端代碼如下:
//UDP_Server.cpp
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
SOCKET srvSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == srvSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
int srvAddrLen = sizeof(srvAddr);
if (SOCKET_ERROR == bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr)))
{
printf("bind Error!\n");
closesocket(srvSock);
WSACleanup();
return -1;
}
char Msg[BUF_SIZE];
int recvLen = 0;
SOCKADDR_IN cltAddr;
memset(&cltAddr, 0, sizeof(cltAddr));
int nCltAddrLen = sizeof(cltAddr);
while (true)
{
printf("Wait Msg From Client...\n");
//無連接的UDP套接字,存在數(shù)據(jù)邊界,sendto和recvfrom必須一一對應(yīng)
//無連接的UDP套接字不會保持連接狀態(tài),因此每次傳輸數(shù)據(jù)都需要添加目標(biāo)地址信息
recvLen = recvfrom(srvSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &srvAddrLen);
if (recvLen == -1)
{
//UDP屬于無連接協(xié)議,因此沒有斷開連接的說法,這里不會進來
printf("Client Disconnected.");
break;
}
Msg[recvLen] = 0;
printf("Receive Msg from Client: %s\n", Msg);
sendto(srvSock, Msg, recvLen, 0, (sockaddr*)&srvAddr, srvAddrLen);
}
closesocket(srvSock);
WSACleanup();
getchar();
return 0;
}
//UDP_Client.cpp
#include "stdafx.h"
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 3)
{
return -1;
}
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup Error!\n");
return -1;
}
SOCKET cltSock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == cltSock)
{
printf("socket Error!\n");
WSACleanup();
return -1;
}
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = inet_addr(argv[1]);
srvAddr.sin_port = htons(_ttoi(argv[2]));
int nSrvAddrLen = sizeof(srvAddr);
//將UDP套接字轉(zhuǎn)換成連接套接字,在此函數(shù)內(nèi)分配cltSock的地址信息
connect(cltSock, (sockaddr*)&srvAddr, sizeof(srvAddr));
char Msg[BUF_SIZE] = "0123456789";
int recvLen = 0;
//客戶端發(fā)送三次數(shù)據(jù),服務(wù)器端調(diào)用一次recvfrom試圖接收三次數(shù)據(jù)。是行不通的
//int nSendLend = 0;
//nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
//nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
//nSendLend += sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, nSrvAddrLen);
while (true)
{
fputs("Input Msg(q to quit): ", stdout);
fgets(Msg, sizeof(Msg), stdin);
if (!strcmp(Msg, "q\n") || !strcmp(Msg, "Q\n"))
{
printf("Disconnected...");
break;
}
//上面已事先分配IP和端口,因此后續(xù)的sendto調(diào)用都不再有分配和刪除地址信息處理,從而提交整體效率。
//sendto(cltSock, Msg, strlen(Msg), 0, (sockaddr*)&srvAddr, sizeof(srvAddr));
//recvLen = recvfrom(cltSock, Msg, BUF_SIZE - 1, 0, (sockaddr*)&srvAddr, &nSrvAddrLen);
//Msg[recvLen] = 0;
//已連接的UDP套接字可以直接使用send 和 recv函數(shù)
send(cltSock, Msg, strlen(Msg), 0);
recvLen = recv(cltSock, Msg, BUF_SIZE - 1, 0);
Msg[recvLen] = 0;
printf("Msg From Server: %s\n", Msg);
}
closesocket(cltSock);
WSACleanup();
getchar();
return 0;
}
總結(jié)
綜上,可總結(jié)UDP服務(wù)器/客戶端的開發(fā)步驟如下:
服務(wù)器端:
①創(chuàng)建服務(wù)器端UDP套接字
②通過bind綁定服務(wù)器端UDP套接字的地址信息
③事先準(zhǔn)備SOCKADDR_IN變量保存往來的客戶端的地址信息
④使用recvfrom、sendto函數(shù)交互數(shù)據(jù)
⑤關(guān)閉服務(wù)器端套接字文章來源:http://www.zghlxwxcb.cn/news/detail-615128.html
客戶端:
①創(chuàng)建客戶端UDP套接字
②初始化服務(wù)器端地址信息
③【可選】調(diào)用connect函數(shù)事先分配客戶端UDP套接字的地址信息
④未事先連接的情況下,可使用sendto、recvfrom函數(shù)與服務(wù)器交互。事先通過connect函數(shù)連接的情況下,還可使用send、recv與服務(wù)器交互
⑤關(guān)閉客戶端套接字文章來源地址http://www.zghlxwxcb.cn/news/detail-615128.html
到了這里,關(guān)于網(wǎng)絡(luò)編程六--UDP服務(wù)器客戶端的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!