UDP通信属于网络通信中的一种方式,需要用套接字来进行通信。
初接触UDP通信时,不知道需要链接静态库#pragma comment(lib,“ws2_32.lib”),导致自己在前期浪费了很多时间去排查问题。除了静态库之外,用到的头文件还有
通信流程如下:
接收端(服务器):
1.创建套接字
2.将套接字绑定到一个本地地址和端口上(bind)
3.等待接受数据(recvform)
4.关闭套接字
发送端(客户端):
1.创建套接字
2.向服务器发送数据(sendo)
3.关闭套接字
各个函数及类型的说明如下:
sockaddr_in addr;//创建一个结构体,用来定义套接字的地址形式,有四个参数,分别代表协议类型、端口号、IP地址,第四个参数不用管,好像是没意义,原型如下:
struct sockaddr_in {
short sin_family; //参数选择AF_INET,代表IPV4,UDP/TCP
u_short sin_port; //确定端口,一般用htons(int a)进行转化
struct in_addr sin_addr; //确定IP
char sin_zero[8];
};
htons(int a); //将主机字节顺序转换为网络字节顺序;
//htons 把unsigned short类型从主机序转换到网络序;
//htonl 把unsigned long类型从主机序转换到网络序
/*addr.sin_addr.s_addr是ip地址,作为服务器,要绑定【bind】到本地的IP地址上进行监听【listen】,
但是机器上可能有多块网卡,也就有多个IP地址,这时候要选择绑定在哪个IP上面,如果指定为INADDR_ANY,
那么系统将绑定默认的网卡【即IP地址】*/
WSAStartup(MAKEWORD(SOCK_VER, 0), &wd);
函数原型: WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData)
WSAStartup,即WSA(Windows SocKNDs Asynchronous,Windows异步套接字)的启动命令。
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。
使用Socket的程序在使用Socket之前必须调用WSAStartup函数。该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中,以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0。
MAKEWORD(a, b)作用是把a和b拼接成为一个无符号的16位整形数,返回的结果是:(a|b<<8)
LOBYTE(a)是取得16进制数最低(最右边)那个字节的内容;HIBYTE(a)是取得16进制数最高(最左边)那个字节的内容。
socket(AF_INET, SOCK_DGRAM, 0);
函数原型:socket(int af, int type, int protocol)
socket函数接受3个参数.
第一个参数(af)指定地址族, 对于TCP / IP协议的套接字, 它只能是AF_INET(也可以写成PF_INET);
第二个参数(type)指定Socket类型, 对于1.1版本的Socket, 他只支持两种类型的套接字, SOCKE_STREAM指定产生流式套接字, SOCK_DGRAM产生数据报套接字;
第三个参数(protocol)是与特定的地址家族相关的协议, 如果指定为0, 那么系统就会根据地址格式和套接类别, 自动选择一个合适的协议.这是推荐使用的一种选择协议的方法.
如果socket函数调用成功, 他就会返回一个新的socket数据类型的套接字描述符; 如果调用失败, 这个函数返回一个INVALID_SOCKET值, 错误信息可以通过WSAGetLastError函数返回.
bind(sockUDP, (sockaddr*)&addr, sizeof(struct sockaddr_in));
函数原型:bind(SOCKET s, const sockaddr *addr, int namelen)
把套接字和端口绑定,成功返回0,服务器进行绑定操作,将一本地地址与一套接口捆绑。
当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
bind函数一般用在接收端,发送端无需配置
sendto(sockUDP, (char*)"wertqw", 3, 0, (sockaddr*)&send2Addr, sizeof(send2Addr));
函数原型:sendto(SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen)
recvfrom(sockUDP, (char*)buf, 1024, 0, (sockaddr*)&saServer, &nFromLen);
函数原型:recvfrom(SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen)
sendto(),是把UDP数据报发给指定地址;recvfrom()是从指定地址接收UDP数据报。
参数说明:
s: socket描述符。
*buf: UDP数据报缓存地址。
len: UDP数据报长度。
flags: 该参数一般为0。
*to: sendto()函数参数,struct sockaddr_in类型,指明UDP数据发往哪里报。
tolen: 对方地址长度,一般为:sizeof(struct sockaddr_in)。
*from: recvfrom()函数参数,struct sockaddr_in类型,指明从哪里接收UDP数据报。
*fromlen 对方地址长度,一般为:&sizeof(struct sockaddr_in)。
函数返回值:
对于sendto()函数,成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
对于recvfrom()函数,成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中。
注:对于接收和发送,个人感觉是:我可以随便接收数据,但你只能向我发送
closesocket(sockUDP);//关闭socket句柄
WSACleanup();//应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源
服务器代码:
void udpServer()
{
WSADATA wsaData;
int err = WSAStartup(MAKEWORD(1, 1), &wsaData);
cout << "UDP server is operating!" << endl;
//创建套接字
SOCKET sockServe = socket(AF_INET, SOCK_DGRAM, 0);
//创建服务器的端口和IP地址
sockaddr_in addrServe={0};
addrServe.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrServe.sin_family = AF_INET;
addrServe.sin_port = htons(6000);
//绑定套接字和端口,服务端绑定操作
bind(sockServe, (SOCKADDR*)&addrServe, sizeof(SOCKADDR));
//创建客户端的端口和IP地址变量,用于存储
sockaddr_in addrClient={0};
int len = sizeof(SOCKADDR);
char recvBuf[100];
memset(recvBuf, 0, 100);
//发送数组暂时屏蔽
//char sendBuf[100];
//memset(sendBuf, 0, 100);
//strcpy_s(sendBuf, "Hello,UDP Client!") ;
while (1)
{
//sockServe从addrClient接收数据
int numBytes=recvfrom(sockServe, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len);
if(numBytes>0)
cout << recvBuf << endl;
}
closesocket(sockServe);
WSACleanup();
}
客户端代码:
void udpClient()
{
WSADATA wsaData;
int err = WSAStartup(MAKEWORD(1, 1), &wsaData);
cout<<"UDP client is operating!"<
第二天的经验:
1.对于既需要接收数据又要发送数据的函数,最好把发送放在接收之后,目的是为了目标地址,通过recvfrom函数可以得到所接收数据的来源地址及端口,并存储到sockaddr *from中,这时,在发送数据(sendto函数)时,只要把目的地址用sockaddr *from表示就可以了。
2.recvfrom函数是阻塞接受的,当一直接收不到数据时,程序就会卡在recvfrom函数处,除非发送一个数据让其接收。解决其阻塞接收的方法:在recvfrom函数前添加下面的代码:
ULONG nVal = 1;
ioctlsocket(clientSock, FIONBIO, &nVal);
//暂未查看代码的作用
3.UDP是一种不可靠的数据传输方式,服务器端/客户端并不需要在连接状态下交换数据,不存在请求连接和受理连接的过程,因此某种意义上并没有明确的服务器端和客户端之分,它的通信只有创建套接字和数据交换的过程,无论服务器端还是客户端都只需要一个套接字即可,且可以实现一对多的通信关系。
4.远程地址指的是对端,本地地址指的是本端,本段端口与对端端口进行数据交换,在不同的函数中,注意区分好本段和对端分别对应的是什么。
5.一般来说,PC有两个IP地址可以用来进行测试,一个是127.0.0.1,另一个是IPv4地址