实现UDP协议的服务端需要经过五步操作:
在加载库时我们使用一个WSAStartup接口函数,它的返回值是int类型,是用来看是否加载成功的,参数有两个,第一个是输入参数,为WORD类型,用来输入版本号,第二个是输出参数,为WSADATA结构体类型,输出参数一般都为指针类型,所以我们要创建三个变量。由于用到的函数和数据类型都是WinSock2.h库中的,所以我们要先加载头文件
#include
#include
using namespace std;
加载库:
int err = 0;
WORD version = MAKEWORD(2, 2);
WSADATA wsaData;
err = WSAStartup(version, &wsaData);
//判断返回值
if (0 != err) {
cout << "WSAStartup error" << endl;
return 1;
}
//判断加载的版本是否是2.2版本
if (2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion)) {
cout << "WSAStartup version error" << endl;
//卸载库
WSACleanup();
return 1;
}else {
cout << "WSAStartup success" << endl;
}
创建套接字我们使用socket()函数,它的返回值为SOCKET类型,如果返回INVALID_SOCKET那么创建失败,我们可以通过WSAGetLastError()来打印错误码
socket()有三个参数,都为int类型,第一个参数af是address family的缩写,我们使用AF_INET(ipv4),第二个参数是type,我们使用Udp协议的类型SOCK_DGRAM,第三个参数是protocol,我们使用UDP协议的IPPROTO_UDP。
SOCKET sock = socket(AF_INET,SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET == sock) {
cout << "socket error:" << WSAGetLastError() << endl;
//卸载库
WSACleanup();
return 1;
}
else {
cout << "socket success" << endl;
}
使用bind()函数,返回值为int类型,如果返回值为SOCK_ERROR那就说明绑定失败了,有三个输入参数,第一个参数为SOCKET,第二个参数为sockaddr*,他是一个结构体指针,第三个参数为指针长度
因为结构体为输入参数,所以我们要为里面的参数赋值,它一共有两个参数,第一个是一个ushort类型,第二个是char数组,那么我们对char数组赋值时会特别麻烦,因为要按照一定的顺序进行赋值,所以这里还给了另一个和sockaddr一样大小的数组——sockaddr_in,这个数组就是将char数组分解成了好几个变量,我们只需要对这几个变量进行赋值就可以了。第一个变量是ip地址类型,我们用的ipv4类型,第二个是端口号,第三个是ip地址
在定义端口号时,由于不同计算机可能存储方式不同,可能是大端存储也可能是小端存储,所以我们有一个规定——网络字节序,是TCP/IP中规定好的一种数据表示格式,可以保证数据在不同主机之间传输时能够被正确解释。用到一个函数htons(),再绑定IP地址时,因为我们是接收所有网卡收到的数据,所以我们对主机内任意网卡都进行绑定。
//是操作系统里面注册端口和ip地址,也就是说当前操作系统收到发给某个端口号和ip地址的数据,就是咱么程序要接收的
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(456789); //转换成网络字节序,也就是大端存储,本机是小端存储
addr.sin_addr.S_un.S_addr = INADDR_ANY; //绑定所有网卡
err = bind(sock,(sockaddr*)&addr,sizeof(addr));
if (SOCKET_ERROR == err) {
cout << "bind error" << endl;
//关闭套接字
closesocket(sock);
//卸载库
WSACleanup();
return 1;
}
else {
cout << "bind success" << endl;
}
接收数据我们使用recvfrom()函数,它的返回值有三种,如果接收数据成功就返回接收到的字节的个数,等于0就证明连接失败了,如果等于SOCK_ERROR就是接收失败了
函数的参数有六个,第一个为socket,意为使用哪个socket进行接收,第二个参数为char*,是一个输出参数,是用来接收数据的缓冲区,第三个参数为这个缓冲区的大小,第四个参数是一个标志位,用来决定当前的接收方式,我们在这里不做特殊设置,用默认的即可,下一个参数也是一个sockaddr *输出类型的参数,用来存放数据是从哪里来的,最后一个参数当然就是上一个参数的长度,但由于它属于是输出类型的参数,所以要变为指针类型
int nRecvNum = 0;
char recvBuf[1024] = "";
sockaddr_in addrClient;
int addrClientSize = sizeof(addrClient);
while (true) {
//4、接收数据
nRecvNum = recvfrom(sock, recvBuf,sizeof(recvBuf),0, (sockaddr*)&addrClient,&addrClientSize);
if (nRecvNum > 0) {
//接收成功,打印一下接收到的数据内容和发送端的ip地址
//"192.168.3.145"十进制四等分字符串类型ip地址
//ulong类型的ip地址:addrClient.sin_addr.S_un.S_addr
cout << "ip:" << inet_ntoa(addrClient.sin_addr) << " say: " << recvBuf << endl;
//从ulong转换成字符串类型ip:inet_ntoa(addrClient.sin_addr);
//从字符串类型转换成ulong类型的ip地址:inet_addr();
}
else {
//接收失败,打印失败日志,结束循环
cout << "recvfrom error" << WSAGetLastError() << endl;
break;
}
}
发送数据使用的是sendto()函数,他也需要卸载循环里,接在上面接收数据后面即可,比如我们发送一个“hahaha”,sendto函数返回值为int类型,如果等于SOCKET_ERROR,那么就是发送失败,它也有六个参数,和接收数据也十分相似,首先是发送用到的socket,然后是发送数据缓冲区和缓冲区大小,然后是标志位,最后是要发送的目标和它的大小,这些都为输入参数
因为我们这里是服务端,所以谁给我们发我们就会给谁一个hahaha,所以目标我们就填接收数据时用来接收的sockaddr
char msg[] = "hahaha";
nSendNum = sendto(sock,msg,sizeof(msg),0,(sockaddr*)&addrClient, addrClientSize);
if (SOCKET_ERROR == nSendNum) {
//发送失败,打印失败日志,结束循环
cout << "sendto error" << WSAGetLastError() << endl;
break;
}
关闭套接字用到的函数为closesocket(),卸载库就是WSACleanup(),这两个函数在上面也都用到过了,这里就不在赘述了
closesocket(sock);
WSACleanup();
现在代码部分我们都写好了,还有一些可能需要的操作,首先我们在尝试运行的时候会发现inet_ntoa会报错,我们可以到项目属性中去将SDL检查关闭即可
再次运行,我们会发现出现了许多无法解析的外部符号的错误,那么是因为编译期找不到函数的实现,那么这些函数都是我们直接调用的,所以解决方法就是加载所需要的库
#pragma comment(lib,"Ws2_32.lib")
那么到此为止,我们的UDP服务端就写好了,测试一下也没什么问题,接下来我们就要写客户端了